Worked Example: Building a Partial @validated Decorator¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Decorator Design Policies Typing"]
page["Worked Example: Building a Partial `@validated` Decorator"]
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"]
The five core lessons in Module 05 become much easier to trust when they all appear in one wrapper that is useful, tempting, and easy to overclaim.
A partial runtime validator is exactly that kind of wrapper.
It combines:
- factory configuration
- signature-aware call binding
- annotation-aware runtime checks
- policy decisions about strictness and failure handling
That makes it the right worked example for the module.
The incident¶
Assume a team wants a @validated decorator for selected callable boundaries.
They want it to:
- read type hints once
- validate supported argument types at call time
- optionally validate returns
- offer strict and warning-based modes
Those are reasonable goals. The danger is pretending this now amounts to full typing or a general validation framework.
The first design rule: keep it partial on purpose¶
This wrapper should be explicit about what it supports and what it refuses.
Supported cases might include:
- plain runtime classes
UnionandOptionalAny
Unsupported cases might include:
- parameterized generics
- deep
Annotatedenforcement - broader typing constructs that really belong to a separate validation framework
That refusal is a design strength, not a lack of ambition.
Step 1: capture configuration and reusable evidence once¶
The factory shape makes the timing explicit:
This means the wrapper can cache:
- the resolved type hints
- the signature
once at definition time, rather than rebuilding them on every call.
That is a good example of factory configuration and evidence caching working together honestly.
Step 2: bind the call before validating¶
One of the most important design choices is to validate after sig.bind(...), not by
guessing from raw args and kwargs.
That makes the validation path:
- bind arguments using Python's own call rules
- apply defaults if needed
- validate the resulting parameter/value mapping
This is stronger and easier to review than ad hoc argument parsing.
Step 3: keep the type checker small and explicit¶
A compact helper such as _is_instance should stay honest about its scope:
AnypassesUnionis checked recursively- unsupported generic hints raise
NotImplementedError
That clarity keeps the wrapper from drifting into fake comprehensiveness.
A bounded implementation¶
import functools
import inspect
import types
import warnings
from typing import Any, Callable, 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 validated(raise_on_error: bool = True) -> Callable:
def decorator(func: Callable) -> Callable:
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]):
msg = (
f"Argument '{name}={value!r}' is "
f"{type(value).__name__}, expected {hints[name]!r}"
)
if raise_on_error:
raise TypeError(msg)
warnings.warn(msg, UserWarning)
result = func(*args, **kwargs)
if "return" in hints and not _is_instance(result, hints["return"]):
msg = (
f"Return '{result!r}' is "
f"{type(result).__name__}, expected {hints['return']!r}"
)
if raise_on_error:
raise TypeError(msg)
warnings.warn(msg, UserWarning)
return result
return wrapper
return decorator
Why this version is useful for review¶
This wrapper is useful because it keeps all the important choices visible:
- configuration is explicit
- signature and hint resolution happen once
- supported hint handling is narrow and readable
- unsupported surfaces are refused clearly
- strict and warning modes are plainly separate
That is the kind of honesty a policy-heavy decorator needs.
Warning mode is not safety mode¶
One especially important design boundary is:
- warning on mismatch does not make the function safe
The wrapped function can still fail internally after the warning. That is exactly why the module frames this as a partial runtime contract rather than as a complete safety system.
What this example makes clear about Module 05¶
This worked example ties the module together:
- factories capture policy once
- binding keeps call matching honest
- annotation-aware checks stay partial
- metadata preservation still matters
- the wrapper is useful only because it refuses to overclaim
That is the durable takeaway. The validator is not here as a universal recipe. It is here as a clear case study in policy ownership and boundary honesty.
The review loop to keep¶
When you inherit or design an annotation-aware decorator, run this loop:
- name the exact hint subset it supports
- verify it binds calls before validating
- check whether strict and warning modes are explicit
- ask whether the policy still belongs in a decorator or should move to a more explicit validator component
If you can do that here, Module 05 has done its job and the course can move into class-level customization with a stronger sense of wrapper limits.
Continue through Module 05¶
- Previous: Wrapper Policy Boundaries
- Next: Exercises
- Reference: Exercise Answers
- Terms: Glossary