Decorators modify functions without changing their code. Here's how they work.
The Basics
A decorator is a function that takes a function and returns a 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 sugar for func = decorator(func).
Preserving Metadata
Use functools.wraps to preserve the original function's name and docstring:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet():
"""Say hello."""
pass
print(greet.__name__) # "greet", not "wrapper"
print(greet.__doc__) # "Say hello."Always use @wraps.
Common Patterns
Timing
import time
from functools import wraps
def timer(func):
@wraps(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)Logging
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapperRetry
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == times - 1:
raise
print(f"Retry {attempt + 1}/{times}")
return wrapper
return decorator
@retry(times=3)
def flaky_api_call():
...Caching
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_computation(n):
# Result cached based on arguments
return sum(range(n))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(times=3)
def say_hi():
print("Hi!")
# Hi!
# Hi!
# Hi!Three levels: factory → decorator → wrapper.
Class Decorators
Decorators can also modify classes:
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, age):
self.name = name
self.age = age
print(User("Owen", 25))
# User(name='Owen', age=25)Stacking Decorators
@decorator_a
@decorator_b
@decorator_c
def func():
pass
# Equivalent to:
func = decorator_a(decorator_b(decorator_c(func)))Order matters. Bottom decorator runs first.
Built-in Decorators
class MyClass:
@staticmethod
def static_method():
# No self parameter
pass
@classmethod
def class_method(cls):
# Gets class, not instance
pass
@property
def computed_value(self):
# Access like attribute
return self._value * 2When to Use Decorators
Good uses:
- Cross-cutting concerns (logging, timing, auth)
- Caching
- Validation
- Registration (plugins, routes)
Avoid when:
- Logic is too complex
- You need to modify the function significantly
- A regular function would be clearer
My Patterns
# Template I use
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Before
result = func(*args, **kwargs)
# After
return result
return wrapperDecorators are powerful but can obscure code. Use them for clear, reusable patterns.
React to this post: