What Is a Decorator?
A decorator is a function that takes another function, adds some behavior, and returns a new function. That's it.
The @ syntax is just shorthand:
@my_decorator
def greet(name):
return f"Hello, {name}"
# Equivalent to:
def greet(name):
return f"Hello, {name}"
greet = my_decorator(greet)
Building Your First Decorator
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
@timer
def slow_function(n):
time.sleep(n)
return n
slow_function(1.5)
# slow_function took 1.503s
The wrapper function:
- Does something before (
start = time.time()) - Calls the original function (
func(*args, **kwargs)) - Does something after (print timing)
- Returns the result
The *args, **kwargs pattern passes through all arguments unchanged.
functools.wraps โ Preserve Metadata
Without functools.wraps, your decorated function loses its name and docstring:
@timer
def my_function():
"""Does something important."""
pass
print(my_function.__name__) # 'wrapper' โ wrong!
print(my_function.__doc__) # None โ wrong!
Fix with @functools.wraps:
import functools
def timer(func):
@functools.wraps(func) # Preserves __name__, __doc__, etc.
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time()-start:.3f}s")
return result
return wrapper
Always use @functools.wraps in real decorators.
Decorators with Arguments
If you want @timer(unit='ms'), you need a decorator factory โ a function that returns a decorator:
def timer(unit='s'):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
value = elapsed * 1000 if unit == 'ms' else elapsed
print(f"{func.__name__} took {value:.1f}{unit}")
return result
return wrapper
return decorator
@timer(unit='ms')
def fast_function():
pass
# fast_function took 0.2ms
Real-World Decorator Examples
Retry with Exponential Backoff
import time
import functools
def retry(max_attempts=3, exceptions=(Exception,), backoff=1.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts - 1:
wait = backoff * (2 ** attempt)
print(f"Attempt {attempt+1} failed. Retrying in {wait}s...")
time.sleep(wait)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, exceptions=(ConnectionError,), backoff=0.5)
def fetch_data(url):
# ... make HTTP request
pass
Cache Results (Memoize)
Python has a built-in for this:
from functools import lru_cache, cache
# Cache with size limit
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2: return n
return fibonacci(n-1) + fibonacci(n-2)
# Cache all results (Python 3.9+)
@cache
def expensive_lookup(key):
return database.get(key) # Only called once per unique key
Access Control
from functools import wraps
def require_auth(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
raise PermissionError("Authentication required")
return func(request, *args, **kwargs)
return wrapper
def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if request.user.role != role:
raise PermissionError(f"Role '{role}' required")
return func(request, *args, **kwargs)
return wrapper
return decorator
@require_auth
@require_role('admin')
def delete_user(request, user_id):
pass
Class-Based Decorators
For stateful decorators, use a class with __call__:
class RateLimiter:
def __init__(self, calls_per_second):
self.calls_per_second = calls_per_second
self.last_called = {}
def __call__(self, func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
last = self.last_called.get(func, 0)
if now - last < 1.0 / self.calls_per_second:
raise RuntimeError("Rate limit exceeded")
self.last_called[func] = now
return func(*args, **kwargs)
return wrapper
rate_limiter = RateLimiter(calls_per_second=10)
@rate_limiter
def api_call():
pass
Key Takeaways
- A decorator is a function that wraps another function:
wrapper = decorator(original) @decoratorsyntax is shorthand forfunc = decorator(func)- Always use
@functools.wrapsto preserve function metadata - For decorators with arguments, add an outer factory function
- Built-in
@lru_cacheand@cachefor memoization โ no need to write your own - Class-based decorators for stateful behavior