Stacking Decorators: Yes, You Can!
Imagine this: You’ve just written a Python function, and you want to add some extra behavior—maybe log its execution time, validate its inputs, and cache its results. Do you rewrite the function three times? Nope! Instead, you stack decorators like fluffy pancakes 🥞, layering them one on top of the other.
Decorators are one of Python’s most elegant features, allowing you to modify or extend functions without changing their core logic. But here’s the fun part: you can use multiple decorators on a single function, combining their powers like a coding superhero. Let’s break it down.
How Decorator Stacking Works
Decorators are applied from the bottom up. Think of them as wrapping paper: the bottom decorator wraps the function first, then the next one wraps that result, and so on.
Here’s a simple example:
@log
@timer
def my_function():
# Do something
pass
This is equivalent to:
my_function = log(timer(my_function))
The order matters! If you reverse them (@timer @log), the behavior changes.
Why Stack Decorators?
Stacking decorators lets you:
✅ Keep code clean – Avoid cluttering your function with extra logic.
✅ Reuse decorators – Apply the same decorator to multiple functions.
✅ Combine functionalities – Mix logging, timing, caching, and more.
Common Decorator Combinations
Here are some powerful pairs (or trios!) you can experiment with:
1. Logging + Timing
Track how long a function runs and log its activity.
def log(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.2f}s")
return result
return wrapper
@log
@timer
def process_data():
# Heavy computation here
pass
2. Validation + Retry
Check inputs and retry on failure.
def validate_input(func):
def wrapper(x):
if x < 0:
raise ValueError("Positive numbers only!")
return func(x)
return wrapper
def retry(max_attempts=3):
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
print(f"Attempt {attempts} failed: {e}")
raise Exception("Max retries reached!")
return wrapper
return decorator
@retry(max_attempts=2)
@validate_input
def calculate_square_root(x):
return x ** 0.5
3. Caching + Rate Limiting
Store results and prevent excessive calls.
from functools import lru_cache
def rate_limit(max_calls=5):
def decorator(func):
call_count = 0
def wrapper(*args, **kwargs):
nonlocal call_count
if call_count >= max_calls:
raise Exception("Rate limit exceeded!")
call_count += 1
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3)
@lru_cache
def fetch_data(url):
# API call here
pass
Watch Out for Pitfalls!
While stacking decorators is powerful, be mindful of:
🔹 Order dependency – @A @B is not the same as @B @A.
🔹 Debugging complexity – Too many layers can make stack traces harder to read.
🔹 Performance overhead – Each decorator adds a tiny delay.
Your Turn: Experiment & Share!
The best way to master decorator stacking? Try it yourself!
- What happens if you stack
@lru_cachewith@timer? - Can you chain four or more decorators?
- What creative combinations can you come up with?
Drop your experiments in the comments—we’d love to see what you build! 🚀