Decorators look like magic, but they're just functions. Here's how they work.
What's a Decorator?
A decorator wraps a function to modify its behavior:
@my_decorator
def my_function():
pass
# Is equivalent to:
def my_function():
pass
my_function = my_decorator(my_function)A Simple Decorator
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
# Calling add
# add returned 5Preserving Function Metadata
Use functools.wraps to keep the original function's name and docstring:
from functools import wraps
def log_calls(func):
@wraps(func) # Preserves __name__, __doc__, etc.
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapperAlways use @wraps in your decorators.
Decorators with Arguments
To pass arguments to a decorator, add another layer:
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")
say_hello()
# Hello!
# Hello!
# Hello!Common Patterns
Timing
import time
from functools import wraps
def timer(func):
@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
@timer
def slow_function():
time.sleep(1)Caching
from functools import wraps
def cache(func):
cached = {}
@wraps(func)
def wrapper(*args):
if args not in cached:
cached[args] = func(*args)
return cached[args]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)(Or just use @functools.lru_cache)
Retry
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def flaky_api_call():
# Might fail sometimes
passAuthentication
from functools import wraps
def require_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
raise PermissionError("Login required")
return func(*args, **kwargs)
return wrapper
@require_auth
def delete_account():
# Only authenticated users can do this
passValidation
from functools import wraps
def validate_positive(func):
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError("Arguments must be positive")
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
return width * heightClass Decorators
Decorate classes too:
def singleton(cls):
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("Connecting...")
db1 = Database() # Connecting...
db2 = Database() # No output - same instance
print(db1 is db2) # TrueMethod Decorators
Works on methods the same way:
class API:
@timer
def fetch_data(self):
# self is passed as first argument
passStacking Decorators
Apply multiple decorators (bottom-up):
@decorator_a
@decorator_b
@decorator_c
def my_function():
pass
# Equivalent to:
# my_function = decorator_a(decorator_b(decorator_c(my_function)))Built-in Decorators
Python provides several useful decorators:
class MyClass:
@staticmethod
def no_self():
# No access to instance
pass
@classmethod
def with_class(cls):
# Access to class, not instance
pass
@property
def computed(self):
# Access like attribute
return self._value
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_computation(n):
# Results are cached
pass
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: floatDecorator Template
Copy this for new decorators:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Before
result = func(*args, **kwargs)
# After
return result
return wrapperWith arguments:
from functools import wraps
def my_decorator(arg1, arg2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Use arg1, arg2 here
return func(*args, **kwargs)
return wrapper
return decoratorDecorators are powerful once you understand the pattern: functions that take functions and return functions. Start with the template and build from there.