When you stack multiple decorators on a single function, you are building a chain of wrappers. The syntax reads top to bottom, but the binding runs bottom to top. The execution at call time flows from the outermost wrapper inward, then back out. Getting this mental model wrong leads to authentication checks that run after the function they are supposed to guard, caching layers that cache the wrong return value, and logging output that records the wrapper's name instead of the original function's.
This article traces what Python does when it encounters multiple @ lines above a single def statement. Every example uses working code you can run and modify. By the end, you will be able to predict the output of any stacked decorator chain without guessing.
What Stacking Means at the Language Level
A single decorator is syntactic sugar. Writing @log above def process() is equivalent to writing process = log(process) after the function body. The decorator receives the function object, and whatever it returns replaces the original name in the enclosing scope.
Stacking adds more layers. When Python sees multiple @ lines, it applies them in sequence starting from the one closest to the def keyword and working outward. PEP 318 chose this order deliberately because it mirrors how function composition works in mathematics: (g ○ f)(x) translates to g(f(x)).
Given this code:
@decorator_a
@decorator_b
@decorator_c
def my_function():
pass
Python executes the equivalent of:
my_function = decorator_a(decorator_b(decorator_c(my_function)))
The innermost call happens first: decorator_c receives the raw function. Its return value is passed to decorator_b. And that return value is passed to decorator_a. The final result of decorator_a is what the name my_function points to from that point forward.
The term "binding" refers to this definition-time process: each decorator binds to the result of the decorator below it. This happens once, when the module loads, not on every function call.
Bottom-Up Binding vs Top-Down Execution
The confusion around stacking comes from conflating two separate phases. Binding happens bottom-up at definition time. Execution happens top-down at call time. These two phases move in opposite directions, and keeping them separate is the key to understanding every stacked decorator chain.
Here is a concrete example that makes both phases visible:
from functools import wraps
def decorator_a(func):
print(f"BINDING: decorator_a receives {func.__name__}")
@wraps(func)
def wrapper_a(*args, **kwargs):
print("CALL: decorator_a BEFORE")
result = func(*args, **kwargs)
print("CALL: decorator_a AFTER")
return result
return wrapper_a
def decorator_b(func):
print(f"BINDING: decorator_b receives {func.__name__}")
@wraps(func)
def wrapper_b(*args, **kwargs):
print("CALL: decorator_b BEFORE")
result = func(*args, **kwargs)
print("CALL: decorator_b AFTER")
return result
return wrapper_b
@decorator_a
@decorator_b
def greet(name):
print(f"Hello, {name}")
return name
print("--- Calling greet ---")
greet("Alice")
Running this produces:
BINDING: decorator_b receives greet
BINDING: decorator_a receives greet
--- Calling greet ---
CALL: decorator_a BEFORE
CALL: decorator_b BEFORE
Hello, Alice
CALL: decorator_b AFTER
CALL: decorator_a AFTER
The binding phase shows decorator_b running first because it is closest to the function. It receives the raw greet function and returns wrapper_b. Then decorator_a receives that wrapper_b (though @wraps preserves the name as greet) and returns wrapper_a.
At call time, the chain reverses. wrapper_a is the outermost layer, so it runs first. Its func(*args, **kwargs) call invokes wrapper_b, which runs its "BEFORE" logic, then calls the original greet, then runs its "AFTER" logic. Control returns to wrapper_a for its "AFTER" logic.
| Phase | Direction | When It Happens | What It Does |
|---|---|---|---|
| Binding | Bottom-up (inner to outer) | Module load / definition time | Each decorator wraps the result of the one below |
| Execution | Top-down (outer to inner) | Every function call | Outermost wrapper runs first, calls inward |
The Onion Model
The clearest way to think about stacked decorators is the onion model. Each decorator adds a layer around the function. The outermost layer is the first one encountered when the function is called, and the original function sits at the center.
For the two-decorator example above, the layers look like this: the outer skin is decorator_a's wrapper, the next layer in is decorator_b's wrapper, and the core is the original greet function.
When you call greet("Alice"), execution peels inward through each layer's "before" logic, hits the core function, then peels back outward through each layer's "after" logic. This is identical to how middleware works in web frameworks like Django and Flask, where request processing flows inward through middleware layers and response processing flows back out.
Here is a three-decorator example that makes the onion structure unmistakable:
from functools import wraps
def layer(name):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f" -> entering {name}")
result = func(*args, **kwargs)
print(f" <- exiting {name}")
return result
return wrapper
return decorator
@layer("outer")
@layer("middle")
@layer("inner")
def process(data):
print(f" ** processing: {data}")
return data.upper()
result = process("hello")
Output:
-> entering outer
-> entering middle
-> entering inner
** processing: hello
<- exiting inner
<- exiting middle
<- exiting outer
The entry flow runs outer, middle, inner. The exit flow reverses: inner, middle, outer. Every stacked decorator chain follows this pattern, regardless of how many layers the stack contains.
If a decorator short-circuits by returning early without calling func(*args, **kwargs), the inner layers and the original function never execute. This is how authentication decorators work: they reject unauthorized requests before the function logic runs.
functools.wraps and Metadata Across the Stack
Every decorator in a stack replaces the function it receives with a new wrapper function. Without @functools.wraps, the metadata of the original function is lost at the first layer. By the time the outermost decorator finishes, the resulting callable's __name__, __doc__, and __qualname__ all belong to the outermost wrapper function.
When every decorator in the stack applies @functools.wraps(func) to its inner wrapper, the metadata propagates through the chain. Each layer copies the metadata from whatever it received, and since the innermost decorator received the original function, that original metadata cascades outward through every layer.
Here is what happens without @wraps:
def timer(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def logger(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
@logger
def compute(x):
"""Return the square of x."""
return x * x
print(compute.__name__) # wrapper
print(compute.__doc__) # None
Both the name and docstring are gone. Now with @wraps on both decorators:
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@timer
@logger
def compute(x):
"""Return the square of x."""
return x * x
print(compute.__name__) # compute
print(compute.__doc__) # Return the square of x.
The __wrapped__ attribute also chains correctly. Calling compute.__wrapped__ returns the logger wrapper, and calling compute.__wrapped__.__wrapped__ returns the original compute function. This chain is how debugging tools and frameworks like Flask can introspect through decorator layers to reach the original callable.
If even one decorator in the stack omits @functools.wraps, the metadata chain breaks at that layer. Every decorator above it will copy the wrapper's metadata instead of the original function's. Treat @wraps as mandatory in every decorator you write.
Ordering Patterns That Matter in Production
In production code, the order of stacked decorators is not arbitrary. The outer decorator's logic runs first on entry and last on exit. This has real consequences for how authentication, logging, caching, and timing interact with each other.
Authentication should be outermost. If an authentication check decorator is stacked above a caching decorator, unauthorized requests are rejected before the cache is consulted. If the order is reversed, the cache serves results to unauthorized callers.
# Correct: auth runs first, blocks unauthorized callers
@require_auth
@cache_result
def get_user_profile(user_id):
return db.query(user_id)
# Wrong: cache serves results even to unauthorized callers
@cache_result
@require_auth
def get_user_profile(user_id):
return db.query(user_id)
Timing should wrap what you want to measure. If you want to measure how long the core function takes without including logging overhead, the timer should be closer to the function than the logger. If you want to measure the entire decorated pipeline including logging, the timer should be outermost.
# Times only the core function, not the logging overhead
@log_calls
@time_execution
def expensive_query(params):
return db.execute(params)
# Times the function plus the logging overhead
@time_execution
@log_calls
def expensive_query(params):
return db.execute(params)
Logging typically goes near the outside. You generally want logs to capture as much context as possible, including the behavior of inner decorators. Placing the logging decorator outermost means it records the full entry and exit of the decorated pipeline.
| Decorator Purpose | Typical Position | Reason |
|---|---|---|
| Authentication / Authorization | Outermost | Reject before any work happens |
| Logging | Near outer | Capture full pipeline behavior |
| Rate Limiting | After auth, before cache | Limit per-user after identity is known |
| Caching | Near inner | Cache only after access checks pass |
| Timing / Profiling | Immediately around target | Measure only what you intend |
| Retry | Innermost | Retry the core call, not the full middleware chain |
Key Takeaways
- Binding is bottom-up, execution is top-down. Decorators are applied starting from the one closest to
defand working outward. At call time, the outermost wrapper runs first. - The onion model is the correct mental model. Each decorator adds a layer. Entry flows inward through layers, hits the core function, and exit flows back outward.
- Every decorator must use
@functools.wraps. In a stack, a single missing@wrapsbreaks the metadata chain for every decorator above it. - Order reflects intent. Authentication outermost, retry innermost, caching after access control, timing immediately around the code you are measuring. The position of a decorator in the stack determines when its logic runs relative to everything else.
- Short-circuiting stops the inward flow. If a decorator returns without calling the wrapped function, no inner decorator and no original function will execute. This is by design for guards like authentication and rate limiting.
Decorator stacking is not a pattern you should avoid. It is a pattern you should understand completely. Once you internalize the binding-vs-execution distinction and the onion model, you can read and write stacked decorators with confidence, predict their output without running the code, and debug ordering problems by tracing the layer each concern occupies in the stack.
Frequently Asked Questions
Do Python decorators execute top to bottom or bottom to top?
Both, depending on the phase. At definition time, decorators bind bottom-to-top: the decorator closest to the def keyword runs first and wraps the raw function. At call time, the resulting wrappers execute top-to-bottom: the outermost decorator's wrapper runs first, then calls inward through each layer.
What happens if a decorator in the middle of the stack does not call the wrapped function?
Every decorator below it and the original function are skipped entirely. The call returns with whatever the short-circuiting decorator provides. This is the mechanism behind authentication and authorization decorators that reject a request before the core logic executes.
Why does PEP 318 specify bottom-to-top application order?
PEP 318 states that the rationale is to match the standard order of function application in mathematics. Composition of (g ○ f)(x) evaluates as g(f(x)). The Python equivalent, @g above @f, evaluates as g(f(func)), which means f is applied first.
Does functools.wraps need to be on every decorator in a stack?
Yes. Each decorator in the chain copies metadata from the function it receives. If any decorator omits @functools.wraps, that layer breaks the metadata chain. Every decorator above it will propagate the wrapper's metadata instead of the original function's __name__, __doc__, and __qualname__.
Should authentication or caching be the outer decorator?
Authentication should be outermost. If caching is outermost, cached results may be served to unauthorized callers because the auth check never runs on cache hits. Placing authentication first ensures every request is verified before any other logic, including cache lookups, proceeds.