Annotation-Aware Runtime Contracts¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Decorator Design Policies Typing"]
page["Annotation-Aware Runtime Contracts"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
orient["Orient on the page map"] --> read["Read the main claim and examples"]
read --> inspect["Inspect the related code, proof, or capstone surface"]
inspect --> verify["Run or review the verification path"]
verify --> apply["Apply the idea back to the module and capstone"]
Annotation-aware decorators are one of the easiest places for a course to overpromise.
Type hints can help wrappers make clearer runtime decisions. They do not turn a decorator into a full type system.
That is the boundary this page keeps explicit.
The sentence to keep¶
When a decorator uses type hints at runtime, ask:
what limited contract is this wrapper checking, and what part of the typing story is it deliberately refusing to claim?
That question keeps runtime validation honest.
The basic runtime pipeline¶
Annotation-aware wrappers typically combine:
typing.get_type_hints(func)to resolve annotationsinspect.signature(func)to understand the callable contractsig.bind(*args, **kwargs)to match actual arguments to parameter names
That sequence matters because it avoids guesswork:
- hints describe expected shapes
- signatures describe callable structure
- binding tells you what was actually passed
This is much stronger than trying to inspect raw args and kwargs ad hoc.
A partial validator is the right review target¶
For Volume I, the honest approach is a partial checker.
Good supported cases:
- plain runtime classes such as
intorstr Union[...]and|of supported typesOptional[T]whenTitself is supportedAnyas an explicit pass-through case
Cases that should be refused or left alone:
- parameterized generics such as
list[int] - protocol-heavy typing features
- the full semantics of
Annotated,Literal, or other advanced typing constructs
This is not a weakness in the module. It is a design choice against pretending runtime checks are broader than they really are.
One picture of the validation path¶
graph TD
decorate["Decoration time"]
cache["Cache signature and resolved hints"]
callSite["Call time"]
bind["Bind args and kwargs"]
check["Check supported hints only"]
delegate["Call original function"]
decorate --> cache --> callSite --> bind --> check --> delegate
Caption: runtime annotation use is strongest when it stays narrow, explicit, and bound to the real call shape.
A minimal helper pattern¶
import functools
import inspect
import types
from typing import Any, Union, get_args, get_origin, get_type_hints
def _is_instance(value: Any, hint: Any) -> bool:
if hint is Any:
return True
origin = get_origin(hint)
if origin in (Union, types.UnionType):
return any(_is_instance(value, arg) for arg in get_args(hint))
if origin is not None:
raise NotImplementedError(
f"runtime type checking for generic types like {hint!r} is not supported here"
)
return isinstance(value, hint)
def validate_args(func):
hints = get_type_hints(func)
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name, value in bound.arguments.items():
if name in hints and not _is_instance(value, hints[name]):
raise TypeError(f"{name}={value!r} does not match {hints[name]!r}")
return func(*args, **kwargs)
return wrapper
This helper is useful because it is honest about its limits. It validates a small subset of hints and refuses the rest instead of silently faking support.
Bound arguments matter here too¶
This page depends directly on Module 03:
- binding gives you the interpreter-faithful mapping of parameter names to values
- defaults can be applied when the validator wants the complete view
Without binding, runtime validation often becomes a brittle mix of tuple indexing and keyword guessing.
That is exactly the kind of shortcut this course is trying to avoid.
Runtime type checks are not static typing¶
This module needs to say this plainly:
- runtime checks happen after code is already running
- they see only the values present at the boundary
- they do not provide the same guarantees as static analysis
So the strongest honest claim is:
this decorator enforces a limited runtime contract at a narrow boundary.
That is a useful claim. It is not the same claim as "this program is type-safe."
Annotated and advanced hints need restraint¶
It is tempting to keep escalating:
Annotatedmetadata as full validation rules- parameterized generics as deep structural contracts
- richer typing constructs as runtime policy engines
That is exactly where a small validator starts turning into a separate validation framework. Sometimes that is justified. Often it is a sign the decorator should stop growing or hand off to a more explicit tool.
Review rules for annotation-aware decorators¶
When reviewing annotation-aware wrappers, keep these questions close:
- what exact hint subset does the wrapper support?
- does the wrapper use
get_type_hintsplus signature binding instead of ad hoc argument guessing? - is unsupported typing surface rejected clearly instead of half-supported?
- does the review describe the result as a partial runtime contract rather than as full typing?
- has the decorator grown large enough that a dedicated validator object or framework would be clearer?
What to practice from this page¶
Try these before moving on:
- Write a validator that supports plain classes,
Union,Optional, andAny. - Force it to encounter
list[int]or another unsupported hint and make it fail clearly. - Explain one useful runtime boundary where partial validation helps and one case where static analysis should still do the heavy lifting.
If those feel ordinary, the next step is cache policy, where wrapper state becomes visible, inspectable, and operationally significant.
Continue through Module 05¶
- Previous: Resilience and Control-Flow Wrappers
- Next: Cache Policy and lru_cache Behavior
- Practice: Exercises
- Terms: Glossary