Python Decorator Execution Order
python decorator execution order: Understand how Python applies decorators at definition time and how stacking order affects runtime behavior, with practical examples...
When you stack multiple decorators on a single function, the order in which they are applied is not the order in which they execute. Python applies decorators bottom-up, meaning the decorator closest to the function runs first during decoration. But when the wrapped function is called, the outermost decorator's wrapper runs first. This distinction between decoration time and call time is the core of python decorator execution order.
The Core Rule: Bottom-Up Application, Top-Down Execution
Decorators are syntactic sugar for passing a function to another function. The expression
@decorator_a @decorator_b def my_func(): pass
is equivalent to
def my_func(): pass my_func = decorator_a(decorator_b(my_func))
During decoration, decorator_b receives the original my_func first, and its return value is passed to decorator_a. So the application order is from the bottom decorator upward. At call time, however, the wrapper returned by decorator_a is the outermost layer, so it runs first, then the wrapper from decorator_b, and finally the original function body.
This asymmetry is the root of most confusion. The decoration order determines the wrapping structure, while the call order follows the nesting from outside in.
Minimal Example: Two Decorators on One Function
Consider two simple decorators that print a message before calling the wrapped function:
def log_entry(func): def wrapper(*args, **kwargs): print("Entering", func.__name__) return func(*args, **kwargs) return wrapper def log_exit(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) print("Exiting", func.__name__) return result return wrapper @log_entry @log_exit def greet(): print("Hello") greet()
When greet() is called, the output is:
Entering greet Hello Exiting greet
Here log_entry is the outermost decorator, so its wrapper runs first. It calls the wrapper from log_exit, which in turn calls the original greet. The decoration order is log_entry(log_exit(greet)); the execution order is log_entry wrapper → log_exit wrapper → greet.
If you reverse the decorator order, the output changes accordingly. This is not just a cosmetic difference; it changes the sequence of side effects and can affect how errors propagate.
Why Decoration Happens at Definition Time
Python evaluates decorators when the function definition is executed, not when the function is called. This means the decorator functions themselves run once, at module import time (or when the definition is reached in a script). The wrappers they return are then bound to the function name.
This has practical implications. If a decorator performs expensive setup, that cost is paid once per function definition, not per call. It also means that the decorated function object is created before any call is made, so you can inspect it, pass it around, or attach attributes to it.
Consider a decorator that registers a function in a registry:
registry = {} def register(func): registry[func.__name__] = func return func @register def process(): pass print(registry) # {'process': <function process at 0x...>}
The registration happens at definition time, so registry is populated as soon as the module is imported. This is a common pattern for plugin systems and dispatch tables.
Ordering Decorators That Take Arguments
Decorators with arguments add another layer of indirection. The decorator factory runs first and returns the actual decorator. The same bottom-up rule applies to the application of the returned decorators.
def repeat(times): def decorator(func): def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator def log(func): def wrapper(*args, **kwargs): print("Calling", func.__name__) return func(*args, **kwargs) return wrapper @log @repeat(3) def say_hi(): print("Hi") say_hi()
Here repeat(3) is evaluated first to produce the decorator, then that decorator is applied to say_hi. The result is passed to log. So the call order is log wrapper → repeat wrapper (which calls the function three times) → original say_hi. The output will show Calling say_hi once, then Hi three times.
If you reversed the order to @repeat(3) above @log, the repeat wrapper would be outermost, and log would run three times, once per inner call. That is a significant behavioral difference, so the order of decorators with arguments must be chosen deliberately.
Common Mistakes with Execution Order
One frequent mistake is assuming that the decorator listed first runs first. As shown, the opposite is true during decoration. Another mistake is ignoring the return value of the inner wrapper. If a decorator does not return the wrapped function's result, the caller receives None instead of the actual return value.
Another subtle issue arises when decorators are applied to methods. The self parameter is passed to the wrapper, and if the wrapper does not accept it or does not forward it correctly, the method call breaks. This is not directly about order, but it often surfaces when stacking decorators that assume a plain function.
Also, forgetting to use functools.wraps can break metadata like __name__ and __doc__. When you stack multiple decorators, each wrapper that lacks functools.wraps replaces the previous function's metadata. The final function may have the name of the innermost wrapper, which is rarely what you want. Using functools.wraps in every decorator preserves the original function's metadata through the entire stack.
from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
Runtime Cost and Performance Considerations
Every decorator adds a layer of function call overhead. When a decorated function is called, the Python interpreter must invoke each wrapper in turn. For most applications, this overhead is negligible compared to the work inside the function. However, for functions that are called millions of times in a tight loop, even a few extra function calls can become measurable.
The decoration itself is a one-time cost. If a decorator performs heavy computation at definition time, that cost is paid once per function, not per call. This is usually acceptable, but be aware that module import time increases if many functions use expensive decorators.
There is also a memory cost: each wrapper holds a reference to the function it wraps, so each decorated function retains the entire chain of wrappers. This is rarely a concern unless you create thousands of decorated functions dynamically.
If performance is critical, you can sometimes avoid decorators altogether by using explicit wrapper functions or by refactoring the logic into a single function. But decorators are often the clearest way to express cross-cutting concerns, and the overhead is usually worth the readability gain.
Maintaining Clarity When Stacking Decorators
The readability of stacked decorators depends heavily on the order. A good rule of thumb is to place decorators that are more general or have broader side effects closer to the top, and decorators that are more specific to the function's behavior closer to the bottom. For example, a logging decorator that wraps all calls should be outermost, while a retry decorator that only affects the function's internal execution can be inner.
When the order is not obvious, add a comment explaining why a particular order was chosen. This helps future maintainers avoid accidentally swapping the decorators and changing the behavior. Also, consider using a single composite decorator that combines several concerns into one wrapper, reducing the number of layers and making the execution order explicit.
For instance, instead of stacking @authenticate, @log, @retry, you could create a @secure_endpoint decorator that applies all three internally in a fixed order. This reduces the chance of misordering at each usage site and centralizes the policy.