How to Chain Multiple Decorators in Python
Python allows you to stack multiple decorators on a single function to combine functionality like authentication, logging, caching, and timing. Understanding the order of execution is crucial for correct behavior.
Understanding the Stack Order
This is the most confusing aspect of decorator chaining:
- Decorating (Bottom-up): The decorator closest to the function wraps it first
- Executing (Top-down): When called, code runs from the outermost decorator inward
@decorator_a
@decorator_b
@decorator_c
def my_function():
pass
# Equivalent to:
my_function = decorator_a(decorator_b(decorator_c(my_function)))
Basic Example: Text Formatting
Stack decorators that add HTML tags:
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
def make_underline(func):
def wrapper():
return f"<u>{func()}</u>"
return wrapper
@make_bold
@make_italic
@make_underline
def greet():
return "Hello World"
print(greet())
Output:
<b><i><u>Hello World</u></i></b>
Execution Trace
greet()returns"Hello World"make_underlinewraps it →"<u>Hello World</u>"make_italicwraps that →"<i><u>Hello World</u></i>"make_boldwraps that →"<b><i><u>Hello World</u></i></b>"
Preserving Function Metadata with functools.wraps
Decorators replace the original function, losing metadata like __name__ and docstrings. Always use @wraps:
from functools import wraps
def logger(func):
@wraps(func) # Preserves original function's metadata
def wrapper(*args, **kwargs):
print(f"Calling: {func.__name__}")
return func(*args, **kwargs)
return wrapper
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
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
@logger
@timer
def process_data(items):
"""Process a list of items."""
return [item * 2 for item in items]
# Metadata is preserved
print(process_data.__name__) # Output: 'process_data'
print(process_data.__doc__) # Output: 'Process a list of items.'
@wrapsWithout @wraps, function introspection breaks:
print(process_data.__name__) # 'wrapper' (wrong!)
print(process_data.__doc__) # None (lost!)
This can break documentation tools, debugging, and frameworks that rely on function names.
Practical Example: Web Endpoint
A realistic example combining authentication, logging, and JSON response formatting:
from functools import wraps
import json
def require_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.get('user'):
return {'error': 'Unauthorized'}, 401
return func(request, *args, **kwargs)
return wrapper
def log_request(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
print(f"Request to {func.__name__} from {request.get('user', 'anonymous')}")
return func(request, *args, **kwargs)
return wrapper
def json_response(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if isinstance(result, tuple):
data, status = result
else:
data, status = result, 200
return json.dumps(data), status
return wrapper
@json_response
@log_request
@require_auth
def get_user_data(request):
"""Fetch user data from database."""
return {'user': request['user'], 'data': 'secret info'}
# Test with authenticated request
request = {'user': 'alice'}
response, status = get_user_data(request)
print(f"Status: {status}, Response: {response}")
Output:
Request to get_user_data from alice
Status: 200, Response: {"user": "alice", "data": "secret info"}
In the example above:
@require_authruns first (innermost) - checks authentication@log_requestruns second - logs the request@json_responseruns last (outermost) - formats the response
If @require_auth fails, the inner decorators never execute.
Decorators with Arguments
Chaining decorators that accept parameters:
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
def prefix(text):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return f"{text}: {result}"
return wrapper
return decorator
@repeat(3)
@prefix("Result")
def generate_number():
import random
return random.randint(1, 100)
print(generate_number())
Output:
['Result: 29', 'Result: 46', 'Result: 22']
Creating a Decorator Chain Helper
For complex applications, create a utility to chain decorators programmatically:
from functools import wraps, reduce
def chain_decorators(*decorators):
"""Chain multiple decorators into one."""
def decorator(func):
# Apply decorators from right to left
return reduce(lambda f, d: d(f), reversed(decorators), func)
return decorator
# Usage
@chain_decorators(logger, timer, require_auth)
def complex_operation(request):
return "Done"
# Equivalent to:
# @logger
# @timer
# @require_auth
# def complex_operation(request): ...
Order Guidelines
| Scenario | Recommended Order (Top to Bottom) |
|---|---|
| Auth + Logging | @log → @auth (log all attempts) |
| Auth + Response | @json_response → @auth (format errors too) |
| Cache + Timer | @timer → @cache (time cache hits) |
| Retry + Timeout | @retry → @timeout (retry timeouts) |
Summary
| Aspect | Direction | Description |
|---|---|---|
| Decorating | Bottom → Top | Closest decorator wraps first |
| Executing | Top → Bottom | Outermost code runs first |
@wraps | Always use | Preserves function metadata |
| Order | Logic-dependent | Auth before formatting, etc. |
- Always use
@wrapsfromfunctoolsto preserve function metadata. - Order matters: Think about which checks should happen first.
- Test individually before chaining to ensure each decorator works correctly.