Decorators look like magic. They're not. Here's how they work.
What's a Decorator?
A decorator is a function that takes a function and returns a new function:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
# Before
# Hello!
# AfterThe @decorator syntax is just sugar for:
say_hello = my_decorator(say_hello)A Practical Example
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:.2f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
slow_function()
# slow_function took 1.00sPreserving Metadata
There's a problem:
@timer
def my_func():
"""My docstring."""
pass
print(my_func.__name__) # wrapper, not my_func!
print(my_func.__doc__) # None!Fix with functools.wraps:
from functools import wraps
def timer(func):
@wraps(func) # Preserves name, docstring, etc.
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.2f}s")
return result
return wrapperAlways use @wraps. It's one line and saves headaches.
Decorators with Arguments
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 greet():
print("Hello!")
greet()
# Hello!
# Hello!
# Hello!The outer function takes the arguments, returns the actual decorator.
Common Use Cases
Logging
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapperAuthentication
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():
...Caching
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 use the built-in:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)Retry Logic
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=2)
def flaky_api_call():
...Stacking Decorators
@log_calls
@timer
@cache
def compute(x):
return x * 2Applied bottom-up: cache first, then timer, then log_calls.
Class Decorators
Decorate a whole class:
def add_repr(cls):
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
@add_repr
class User:
def __init__(self, name, email):
self.name = name
self.email = email
print(User("Owen", "owen@example.com"))
# User(name='Owen', email='owen@example.com')Method Decorators
For methods, remember self:
def log_method(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
print(f"{self.__class__.__name__}.{func.__name__} called")
return func(self, *args, **kwargs)
return wrapper
class Calculator:
@log_method
def add(self, a, b):
return a + bBuilt-in Decorators
Python includes several:
class MyClass:
@property
def name(self):
return self._name
@staticmethod
def utility():
pass
@classmethod
def from_string(cls, s):
return cls(s)
from functools import lru_cache, cached_property
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: intWhen to Use
Use decorators for cross-cutting concerns:
- Logging
- Timing
- Caching
- Authentication/authorization
- Validation
- Retry logic
Don't use decorators when:
- Simple inheritance works better
- The logic is specific to one function
- It makes code harder to understand
Decorators should be reusable across multiple functions.