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.
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.
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.
Stacked decorators compose predictably¶
Multiple decorators apply from the bottom up:
desugars to:
So the final binding is:
That is why definition-time application order and call-time execution order are related but not identical.
One picture of stacking¶
Definition time:
original f -> d1(f) -> d2(d1(f)) -> 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.
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
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.
Decorator factories add one more evaluation step¶
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 decoration happen once when the function is defined, not on every invocation.
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.
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?
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