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.
Treat this page as a guided trace of the shipped implementation, not as a recipe to paste unchanged into production. Its teaching job is to show where a validator decorator stays honest, where it starts overpromising, and what evidence a reviewer should demand before accepting it.
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.
Review questions for the page¶
Keep two questions active while reading:
- what exact runtime promise is this decorator allowed to make?
- what stronger typing or safety promise must it refuse?
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.
That sentence is the real incident. The syntax is easy. The hard part is stopping the team from calling a bounded runtime checker "type safety" or "complete validation."
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.
Add one more refusal boundary: this decorator does not try to infer business meaning from annotations. It checks a narrow runtime shape. It does not decide whether the overall API contract is sensible.
Non-goals to keep visible¶
This worked example is intentionally not trying to deliver:
- a full runtime type system
- coercion or conversion rules
- cross-parameter semantic validation
- nested container validation
- framework-grade error reporting
If a review conversation expects those features, the design already wants a more explicit validation owner than this module's decorator boundary.
Establish the baseline¶
From programs/python-programming/python-meta-programming, run:
Keep these files side by side:
labs/decorator_policy/validation.pytests/test_decorator_policy_validation.py- the
validationobject printed by the lab
The source tells you who owns the behavior. The tests prove its boundaries. The JSON shows the learner-visible consequences. No one surface is enough by itself.
Step 1: capture configuration and reusable evidence once¶
The shipped 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.
It is also the first place a review should look for hidden cost. If hint resolution is expensive, environment-sensitive, or brittle under forward references, the wrapper should say so plainly instead of pretending definition-time capture is always harmless.
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.
It also gives the reviewer a clean proof route:
| Evidence step | What it proves |
|---|---|
inspect.signature(func) |
the validator uses Python's call model as its starting point |
sig.bind(*args, **kwargs) |
argument matching follows real parameter rules |
bound.apply_defaults() |
omitted values still become visible to the validator |
iterating bound.arguments.items() |
checks are attached to named parameters, not positional guessing |
Step 3: keep the type checker small and explicit¶
A compact helper such as _matches_hint should stay honest about its scope:
AnypassesUnionis checked recursively- unsupported generic hints have already caused
UnsupportedHintErrorduring decoration
That clarity keeps the wrapper from drifting into fake comprehensiveness.
The helper should also make unsupported space noisy. Quietly skipping unsupported hint forms teaches the wrong lesson, because it makes the wrapper look broader than it is.
Step 4: expose the added policy¶
functools.wraps exposes the original callable through __wrapped__, but it does not
describe the new validation contract. The implementation therefore attaches:
The focused test inspects those surfaces alongside inspect.signature,
inspect.unwrap, __name__, and __doc__. The wrapper is transparent about both the
callable it wraps and the policy it adds.
These attributes are intentionally local to the lab. In a library, prefer an ordinary public inspection function or named protocol over inventing dunder attributes casually.
Step 5: trace each call route¶
Strict success:
Strict argument failure:
Warning mismatch:
Strict return failure:
Unsupported generic:
What this implementation proves and what it does not¶
Use the code block as evidence, not as marketing:
| Claim | Supported by this implementation? | Why |
|---|---|---|
| it validates a narrow runtime hint subset | yes | _validate_supported_hint and _matches_hint name and limit the supported cases |
| it follows Python's own call binding rules | yes | sig.bind(...) does the call matching |
| it can raise or warn on mismatches | yes | on_mismatch makes the mode visible |
| it enforces full static typing semantics | no | many typing surfaces are refused intentionally |
| it makes warning mode safe | no | the wrapped function still runs after the warning |
| it is already a full validation framework | no | there is no broader owner, registry, coercion model, or reporting layer |
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.
Failure surfaces a reviewer should inspect¶
Even this bounded version has pressure points:
| Failure surface | Why it matters |
|---|---|
get_type_hints(func) can fail or resolve differently than expected |
definition-time evidence may not be as stable as the wrapper claims |
UnsupportedHintError on generic hints |
refusal is honest and happens when the decorator is applied |
| warning mode lets execution continue | downstream failures may happen after the contract warning |
| return validation happens after function execution | bad outputs are detected late, not prevented early |
Those are not bugs in the teaching example. They are the boundaries the teaching example must keep visible.
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.
Use this boundary table when someone overstates the mode switch:
| Mode | What it changes | What it does not change |
|---|---|---|
on_mismatch="raise" |
mismatch becomes an immediate TypeError |
the decorator still only understands a limited hint subset |
on_mismatch="warn" |
mismatch becomes an observable warning | the underlying function may still break or produce a bad result |
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.
Evidence packet to leave behind¶
If you adapt this example, leave behind a packet another reviewer can inspect quickly:
- the supported hint subset
- the refused hint subset
- the exact behavior of strict mode
- the exact behavior of warning mode
- one sentence explaining why the policy still belongs in a decorator
- one sentence naming the next threshold that would justify an explicit validator object
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
Add one last question:
- which unsupported feature would be the first sign that this wrapper has outgrown the decorator boundary?
Proof ledger¶
Use the test names as a claim ledger:
| Claim | Focused proof |
|---|---|
| defaults are bound and bad arguments do not execute | test_strict_mode_binds_defaults_and_rejects_wrong_argument_types |
Union, Optional, and Any are in the supported subset |
test_union_optional_and_any_form_the_supported_non_class_surface |
| parameterized generics are refused during decoration | test_parameterized_generics_are_refused_when_the_function_is_decorated |
| warning mode executes | test_warning_mode_observes_a_mismatch_but_still_executes |
| return rejection happens after execution | test_strict_return_validation_runs_after_the_wrapped_function |
| policy and callable evidence remain visible | test_wrapper_exposes_policy_hints_and_original_signature |
If you change the implementation, identify the affected claim before changing a test. That keeps the test suite from becoming an after-the-fact snapshot.
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.
Exit check for this page¶
Before leaving the worked example, make sure you can do all of these:
- explain why
sig.bind(...)is a stronger review choice than rawargsinspection - name two supported hint forms and two refused hint forms
- explain why warning mode is still operationally risky
- say when this decorator should be replaced by an explicit validation component
- map every claim you make about the implementation to one focused test
Continue through Module 05¶
- Previous: Wrapper Policy Boundaries
- Next: Exercises
- Reference: Exercise Answers
- Terms: Glossary