Back to Blog
Python

Python Nested Decorator: How Stacking Works

python nested decorator: Learn how Python nested decorators work, how stacking changes execution order, and how to preserve function metadata with functools.wraps.

decoratorsfunction wrappingpython functionscode reusefunctools.wraps
Diagram showing nested decorators wrapping a function in layers.

Python nested decorator refers to applying more than one decorator to a single function. The syntax is straightforward, but the order in which decorators wrap the function has a significant effect on runtime behavior. A nested decorator stack executes from the bottom up during decoration, and from the top down when the wrapped function is called.

What a Nested Decorator Is

A decorator is a callable that takes a function and returns a new function. When you stack decorators, each decorator receives the result of the one below it. For example:

@decorator_a @decorator_b def my_function(): pass

This is equivalent to:

def my_function(): pass my_function = decorator_a(decorator_b(my_function))

The decorator closest to the function (decorator_b) is applied first, then decorator_a wraps the result. This means the outermost decorator (decorator_a) is the one that runs first when the function is called, because it wraps everything else.

How Decorator Order Affects Execution

The order matters because each decorator can add behavior before or after the wrapped function runs. Consider two simple decorators that print messages:

def log_entry(func): def wrapper(*args, **kwargs): print("entering") return func(*args, **kwargs) return wrapper def log_exit(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) print("exiting") return result return wrapper

If you apply them as:

@log_entry @log_exit def greet(name): print(f"hello {name}")

The call greet("Ada") prints:

entering
hello Ada
exiting

If you swap the order, the output changes. Understanding this order is essential when you build decorators that must coordinate, such as authentication and logging.

The application and execution order can be summarized as follows:

PositionApplication orderExecution order
Top decoratorApplied lastRuns first
Bottom decoratorApplied firstRuns last

Building a Practical Nested Decorator Stack

A common use case is combining timing and logging. The timing decorator measures execution time, and the logging decorator records the call. Because the timing decorator wraps the logging decorator, the timing measurement includes the logging overhead. That is usually acceptable, but you should be aware of it.

import time import functools def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper def logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"calling {func.__name__} with args={args} kwargs={kwargs}") return func(*args, **kwargs) return wrapper @timer @logger def process_data(data): # simulate work return sum(data)

When you call process_data([1, 2, 3]), the timer wrapper runs first, then the logger wrapper, then the actual function. The elapsed time includes the logging call.

Passing Arguments Through Nested Decorators

Decorators can accept their own arguments, which adds another layer of nesting. For example, a retry decorator that takes a retry count:

def retry(times): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(times): try: return func(*args, **kwargs) except Exception: if attempt == times - 1: raise return None return wrapper return decorator

When you stack this with another decorator, the order of application still follows the same rule. The decorator factory runs first, then the inner decorator wraps the function.

Preserving Function Metadata with functools.wraps

Without functools.wraps, a decorated function loses its original name, docstring, and signature. When you stack multiple decorators, the metadata loss compounds. Using functools.wraps in each wrapper preserves the original function's metadata, which is critical for debugging and introspection.

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

Always apply functools.wraps in every decorator you write, especially when they are meant to be stacked.

Performance and Maintainability Considerations

Each decorator adds a function call layer. For most applications, the overhead is negligible, but in hot paths it can matter. If you have many decorators, each call passes through multiple wrappers, which increases call stack depth and can make profiling more confusing. Maintainability suffers when decorators have hidden side effects or depend on the order in which they are applied. Keep decorators small and focused, and document the expected order when you expose them as part of a library.

Common Mistakes When Stacking Decorators

One common mistake is forgetting that the order of application is bottom-up. Another is assuming that decorators can be applied in any order without changing behavior. A third mistake is not using functools.wraps, which makes stacked decorators harder to debug. Also, be careful when a decorator returns a different callable type, such as a class instance, because subsequent decorators may not handle it correctly.

python nested decorator: Practical Usage and Code Examples | RYUSLOG DEV