Decorator Syntax and Definition-Time Rebinding¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Meta-Programming"]
section["Function Wrappers Transparent Decorators"]
page["Decorator Syntax and Definition-Time Rebinding"]
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"]
Once the wrapper skeleton feels ordinary, the next step is to remove the last bit of surface mystique:
@decoratorsyntax is just rebinding at definition time.
That sentence is the foundation for understanding stacked decorators, decorator factories, and the difference between one-time transformation work and per-call behavior.
It is also the page where a common confusion must end:
@decoratoris not "special syntax that runs around every call"@decoratoris a definition-time rebinding step that determines what callable later receives the call
The sentence to keep¶
When you see @decorator, ask:
what expression was evaluated at definition time, and what name was rebound to the returned wrapper?
That question makes the timing and ownership explicit immediately.
If you can answer that question clearly, stacked decorators stop feeling like folklore and start feeling like ordinary evaluation order.
Single decorators desugar to assignment¶
This:
means:
The original function object is created first. Then the decorator is applied. Then the
name f is rebound to the returned callable.
That sequence matters because decoration happens once, not on every later call.
That one-time timing is not a side note. It affects:
- import-time side effects
- configuration capture in decorator factories
- when failures appear
- whether later behavior is controlled by one-time setup or per-call logic
Stacked decorators have two definition-time orders¶
Multiple returned decorators apply from the bottom up:
desugars to:
So the final binding is:
There is one earlier order that must not be skipped: decorator expressions are evaluated from top to bottom before any returned decorator is applied. With decorator factories, the complete sequence is:
- evaluate
d3 - evaluate
d2 - evaluate
d1 - create the raw function
- apply
d1 - apply
d2 - apply
d3
For plain names, expression evaluation is usually invisible. For calls such as
@tracing_decorator("outer", events), evaluation runs the factory body and can validate
configuration or cause effects. Collapsing expression evaluation and decorator
application loses a real runtime boundary.
That is why definition-time expression order, definition-time application order, and call-time execution order are related but not identical.
Use that distinction as a review check:
| Question | Definition-time answer | Call-time answer |
|---|---|---|
| which decorator expression evaluates first? | the highest expression in the stack | not applicable |
| which returned decorator applies first? | the lowest decorator in the stack | the outermost resulting wrapper runs first |
| where do configuration expressions run? | once, while the function is being defined | not again unless the wrapper itself re-evaluates something |
| where do wrapper-local side effects happen? | only if the decorator performs them during construction | on each later call if the wrapper body performs them |
One picture of stacking¶
Definition time:
evaluate d3 -> evaluate d2 -> evaluate d1 -> create original f
apply d1(f) -> apply d2(d1(f)) -> apply d3(d2(d1(f)))
Call time:
caller -> outermost wrapper d3 -> d2 -> d1 -> original function
This is one of the most useful review diagrams in the module because it keeps timing and composition straight.
Guided trace: predict before executing¶
The course program records all three orders:
$ make wrapper-lab |
python3 -c 'import json, sys; p=json.load(sys.stdin)["lifecycle"]; print(*p["definition_events"], *p["call_events"], sep="\n")'
factory:outer
factory:inner
decorate:inner:render_status
decorate:outer:render_status
enter:outer
enter:inner
body:INC-42:critical
exit:inner
exit:outer
Read the trace in ownership order:
- both factories run before the raw function is rebound
- the inner returned decorator first receives the raw function
- the outer returned decorator receives the inner wrapper
- the later call enters the outer layer first
- the return path unwinds from inner to outer
Now change the source order of the two @tracing_decorator(...) lines. Predict the entire
nine-event trace before running the program. Restore the published order and run
make wrapper-lab-test.
The focused test asserts exact order because order is the teaching contract. A test that only checked the final string would miss every definition-time distinction this core exists to teach.
A simple example¶
def uppercase(func):
def wrapper(text):
return func(text).upper()
return wrapper
@uppercase
def greet(name):
return f"Hello, {name}!"
print(greet("Alice"))
The important point is not the output. It is the rebinding:
- the raw
greetfunction existed first uppercase(greet)produced a wrapper- the name
greetnow points to that wrapper
For a missed-class learner, that rebinding sentence is the durable fact to keep. If the code is confusing later, rewrite the decorated form back into assignment form and inspect the returned callables one step at a time.
Stacked wrappers show both definition-time and call-time order¶
def add_exclaim(func):
def wrapper(text):
return func(text) + "!"
return wrapper
def trim(func):
def wrapper(text):
return func(text.strip())
return wrapper
@add_exclaim
@trim
@uppercase
def message(text):
return f"{text} world"
Definition time:
uppercaseapplies first to the rawmessagetrimwraps that resultadd_exclaimwraps the result of that
Call time:
- the outermost wrapper runs first
- control flows inward to the original function
- the return value flows back outward
That is why decorator order is never cosmetic.
It also explains why "the same decorators are present" is not enough to claim the same behavior. The stack order is part of the behavior.
Decorator factories make expression evaluation visible¶
A factory such as @factory(config) means:
- evaluate
factory(config)once at definition time - treat the result as the actual decorator
- apply that decorator to the function
So:
means:
This is another useful timing lesson: both the factory call and the returned decorator application happen once when the function is defined, not on every invocation. They are still separate operations with different ordering rules.
That is where import-time surprises often enter. If factory(arg) does expensive work,
opens resources, or validates configuration, those costs happen while the definition is
being evaluated, not lazily on the first later call.
Definition time versus call time is a real review boundary¶
By this point in the course, that boundary should stay explicit:
- definition time: decorator expressions evaluate and wrappers are built
- call time: wrapper logic runs around the original function
If a code review blurs those together, it becomes much harder to reason about imports, state initialization, and wrapper overhead.
Definition-time mistakes worth catching early¶
The following mistakes are more common than they first appear:
| Weak explanation or bug | What it confuses | Repair |
|---|---|---|
| "the decorator runs every time I call the function" | the decorator itself versus the wrapper it returned | separate rebinding from later wrapper execution |
| "the factory argument is dynamic per call" | captured definition-time configuration versus call-time input | show where factory(arg) was evaluated once |
| "stacked decorators are basically left to right" | source order versus application order | rewrite them as nested calls or rebinding assignments |
| "all definition-time work is bottom-up" | decorator-expression evaluation versus decorator application | trace factory calls top-down, then application bottom-up |
| "the error came from inside the function" | definition-time failure versus call-time failure | check whether the decorator expression failed before the function was ever invoked |
Non-callable decorator expressions fail early¶
Because decoration is ordinary application of a callable, invalid decorator expressions break at definition time:
- non-callable decorator object
- broken factory result
- errors inside the decorator itself
That is a useful reminder that decorators are ordinary runtime behavior, just happening at definition time instead of later at call time.
Review rules for decorator syntax and timing¶
When reviewing decorator-heavy code, keep these questions close:
- what raw function existed before rebinding happened?
- what exactly got evaluated at definition time?
- in what order did stacked decorators compose?
- what work happens only once versus on every call?
- is a decorator factory being treated as if it were a per-call configuration step when it is not?
Exit check for this page¶
Before leaving this page, make sure you can do all of these without hand-waving:
- rewrite one decorated function into explicit rebinding assignments
- explain why bottom-up application still leads to outermost-first call execution
- show where a decorator factory captured configuration once
- name one failure that would happen at definition time rather than at call time
What to practice from this page¶
Try these before moving on:
- Rewrite one
@decoratorexample by hand asf = decorator(f). - Desugar one stacked decorator example into its step-by-step rebinding order.
- Write one decorator factory and explain when the factory runs versus when the wrapper runs.
If those feel ordinary, the next step is practical thin wrappers that change call-time behavior while still trying to stay transparent.
Continue through Module 04¶
- Previous: Nested Functions and Wrapper Skeletons
- Next: Thin Practical Wrappers at Call Time
- Practice: Exercises
- Terms: Glossary