Back to Blog
Python

Python Multiple Decorators: Order and Composition

python multiple decorators: Learn how Python multiple decorators stack and execute, including application order, call-time behavior, functools.wraps, and when to combi...

python decoratorsfunction wrappingdecorator orderfunctoolscode composition
Illustration of multiple nested decorator layers wrapping a Python function, showing execution flowing from the outer layer inward.

When you apply python multiple decorators to a single function, the order in which they are listed determines both how they wrap the function and how they execute at call time. This ordering is one of the most common sources of confusion when working with stacked decorators.

How Python Applies Multiple Decorators

Consider this definition:

@decorator_a @decorator_b def my_function(): pass

Python applies decorator_b first, then passes the result to decorator_a. The final value bound to my_function is the return value of decorator_a. In other words, the decorator closest to the function definition runs first at decoration time.

This means the decorator listed at the top is applied last. If you read the stack from top to bottom, the application order is bottom-up.

Execution Order at Call Time

At call time, the order reverses. When you invoke my_function(), the outermost wrapper—the one created by decorator_a—executes first. It then calls the wrapper created by decorator_b, which eventually calls the original function body.

The asymmetry between application order and execution order is the root of most confusion. The decorator listed first is applied last, but it runs first when the function is called.

A Minimal Example

The behavior becomes clear with a small demonstration:

def outer(func): def wrapper(*args, **kwargs): print("outer before") result = func(*args, **kwargs) print("outer after") return result return wrapper def inner(func): def wrapper(*args, **kwargs): print("inner before") result = func(*args, **kwargs) print("inner after") return result return wrapper @outer @inner def greet(name): print(f"Hello, {name}") greet("Alice")

The output is:

outer before
inner before
Hello, Alice
inner after
outer after

The outer wrapper runs first, then inner, then the original function body. After the function returns, control unwinds in reverse order: inner finishes its post-call work, then outer finishes.

Practical Example: Validation and Logging

A realistic use of multiple decorators is combining validation with logging:

import functools import logging logger = logging.getLogger(__name__) def log_calls(func): @functools.wraps(func) def wrapper(*args, **kwargs): logger.info("Calling %s", func.__name__) result = func(*args, **kwargs) logger.info("Finished %s", func.__name__) return result return wrapper def require_non_negative(func): @functools.wraps(func) def wrapper(*args, **kwargs): for arg in args: if isinstance(arg, (int, float)) and arg < 0: raise ValueError("Arguments must be non-negative") return func(*args, **kwargs) return wrapper @log_calls @require_non_negative def square(x): return x * x

With this ordering, require_non_negative runs before log_calls records the invocation. Invalid input raises before the log entry is written. If you reversed the decorators, the log would record a call that then fails validation, changing the observable behavior of the logging. Choose the order based on which concern should observe the other's outcome.

Common Mistakes with Decorator Order

Assuming Top-Down Execution

Developers new to stacked decorators often assume the first decorator listed runs first. As shown above, the decorator closest to the function runs first at decoration time and last at call time. This mistake produces confusing behavior when decorators have side effects, such as registering handlers or modifying global state.

Losing Function Metadata

If any decorator in the stack omits functools.wraps, the wrapped function's __name__, __doc__, and __module__ are replaced by the wrapper's attributes. When multiple decorators omit it, each layer overwrites the metadata from the layer below. The final function loses its original identity, which complicates debugging, documentation generation, and test frameworks that rely on function names.

Decorator Side Effects at Import Time

Decorator functions execute when the module is imported, not when the wrapped function is called. If a decorator performs expensive setup—such as opening a connection or loading configuration—that cost is paid at import time. Stacking several such decorators multiplies the import-time work, which can slow down application startup noticeably.

Runtime Cost and Performance Considerations

Each decorator adds a wrapper layer, and every call passes through all wrappers. For typical application code, this overhead is negligible: a few extra function calls per invocation. But in performance-sensitive code, where a function is called millions of times, the cumulative cost of several wrappers becomes measurable.

The overhead comes from the additional stack frames and attribute lookups each wrapper introduces. If you profile and find that decorator overhead matters, consider merging multiple concerns into a single decorator. A single wrapper that performs validation, logging, and timing in one pass avoids the cost of multiple nested function calls.

When to Combine Concerns into One Decorator

Stacking decorators is readable when each decorator has a single responsibility. But when decorators are always used together, or when their behavior is interdependent, combining them into one decorator can reduce confusion and overhead.

Use separate decorators when they are independently reusable in different combinations. For example, a @retry decorator and a @timeout decorator are useful on their own and in combination with other decorators. Merge them when you always apply them together and the combined behavior is easier to reason about as a unit.

Using functools.wraps in Every Layer

Every decorator in the stack should use functools.wraps. This preserves the original function's metadata through all layers:

import functools def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper

Without functools.wraps, the final __name__ of the decorated function is the innermost wrapper's name. Stack traces, documentation tools, and test frameworks all depend on accurate function metadata. Preserving it through every layer keeps the decorated function introspectable and debuggable, even when several decorators are stacked. When you write a decorator intended for reuse, always include functools.wraps so that it composes correctly with other decorators in the same stack.

python multiple decorators: Practical Usage and Code Example | RYUSLOG DEV