Skip to main content

How to Chain Decorators to Functions in Python

Decorators allow you to wrap a function to extend its behavior without modifying its source code. Chaining decorators (also known as stacking or nesting) enables you to apply multiple layers of functionality to a single function. This is essential for creating modular code where concerns like logging, authentication, and caching can be mixed and matched.

This guide explains the execution order of chained decorators and how to implement them effectively.

Understanding Decorator Execution Order

When you stack multiple decorators, it is crucial to understand the order in which they run.

  • Application Order: Bottom to Top.
  • Execution Order: Outermost wrapper starts first, calls the inner wrapper, which calls the function.

Think of it like an onion: @decorator1 wraps @decorator2, which wraps the function.

Method 1: Stacking Decorators

Let's create two simple decorators to visualize the flow: one that emphasizes text (bold) and one that italicizes it.

def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper

def make_italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper

# ✅ Correct: Stacking decorators
# Order: make_bold(make_italic(hello))
@make_bold
@make_italic
def hello():
return "Hello World"

print(hello())

Output:

<b><i>Hello World</i></b>
note

If you swapped the order (@make_italic on top), the output would be <i><b>Hello World</b></i>. The decorator closest to the function definition (def) is applied first.

Method 2: Using Decorators with Arguments

Real-world decorators often accept arguments. Chaining these works exactly the same way, but provides dynamic control over the wrapping logic.

import functools

def repeat(times):
"""Decorator that repeats the function call specific times."""
def decorator_repeat(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
value = func(*args, **kwargs)
return value
return wrapper
return decorator_repeat

def log_call(func):
"""Decorator that logs the function call."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Logging: Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper

# ✅ Correct: Chaining a parameterized decorator with a standard one
@repeat(times=2)
@log_call
def greet(name):
print(f"Hello, {name}!")

greet("Alice")

Output:

Logging: Calling greet
Hello, Alice!
Logging: Calling greet
Hello, Alice!

Method 3: Preserving Metadata

When chaining decorators, the inner function's metadata (like its name and docstring) can get lost or obscured by the wrapper functions. Using functools.wraps is critical when chaining to ensure debugging tools see the original function, not wrapper.

import functools

def debug(func):
@functools.wraps(func) # Preserves metadata
def wrapper(*args, **kwargs):
print(f"[DEBUG] Executing {func.__name__}")
return func(*args, **kwargs)
return wrapper

def auth(func):
@functools.wraps(func) # Preserves metadata
def wrapper(*args, **kwargs):
print("[AUTH] Verifying permissions...")
return func(*args, **kwargs)
return wrapper

@debug
@auth
def delete_database():
"""Deletes the main database."""
print("Database deleted.")

# Verify metadata is intact
print(f"Function Name: {delete_database.__name__}")
print(f"Docstring: {delete_database.__doc__}")

Output:

Function Name: delete_database
Docstring: Deletes the main database.
warning

Without @functools.wraps, delete_database.__name__ would likely return wrapper (the name of the inner function in debug), making debugging significantly harder.

Conclusion

Chaining decorators is a clean way to compose behavior.

  1. Order Matters: Decorators stack from bottom to top (innermost first).
  2. Wrappers: The logic runs from top to bottom (outermost wrapper starts first).
  3. Best Practice: Always use @functools.wraps to keep your function metadata intact through the chain.