Decorators
What are decorators?
A decorator is a function that wraps another function to add or modify behavior without changing the wrapped function’s code. In Python syntax, decorators are written with the @decorator_name line above a function definition.
Under the hood, this:
@decorator
def func():
...
is equivalent to:
def func():
...
func = decorator(func)
Decorators are a practical application of higher-order functions: they take a function as input and return a new function.
Why this matters
Decorators let you:
- Reuse cross-cutting behavior like logging, timing, caching, and access control.
- Keep business logic clean by moving boilerplate out of your core functions.
- Compose behavior by stacking multiple decorators.
They show up everywhere in real-world Python:
@classmethod,@staticmethod,@propertyin classes@app.route("/path")in web frameworks@pytest.mark.parametrizein tests
Understanding decorators is essential for reading and writing modern Python.
A simple decorator
Let’s start with a minimal decorator that prints before and after a function call.
def debug(func):
"""Print the function name and arguments each time it's called."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
Use it with @debug:
@debug
def add(a, b):
return a + b
add(2, 3)
# Calling add with args=(2, 3), kwargs={}
# add returned 5
Here:
debugtakesfuncas an argument.- It defines
wrapper, which callsfuncand adds extra behavior. - It returns
wrapper, which becomes the new version ofadd.
Decorators and *args, **kwargs
Real-world decorators should work with any function signature. That’s why decorators almost always use *args and **kwargs.
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"→ {func.__name__} called")
result = func(*args, **kwargs)
print(f"← {func.__name__} finished")
return result
return wrapper
This decorator can wrap functions with any parameters:
@log_calls
def greet(name, punctuation="!"):
print(f"Hello, {name}{punctuation}")
greet("Alice")
greet("Bob", punctuation="!!!")
Preserving metadata with functools.wraps
By default, the wrapper function hides the original function’s name and docstring:
@debug
def multiply(a, b):
"""Multiply two numbers."""
return a * b
print(multiply.__name__) # 'wrapper', not 'multiply'
print(multiply.__doc__) # None
Use functools.wraps to copy metadata from the original function to the wrapper:
import functools
def debug(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
Now:
@debug
def multiply(a, b):
"""Multiply two numbers."""
return a * b
print(multiply.__name__) # 'multiply'
print(multiply.__doc__) # 'Multiply two numbers.'
Always use @functools.wraps in real decorators unless you have a very specific reason not to.
Decorators with arguments
Sometimes you want to configure a decorator, e.g., set a logging level or a label. This means the decorator itself needs parameters.
Pattern:
- Outer function: takes decorator arguments, returns the real decorator.
- Decorator: takes the function and returns a wrapper.
- Wrapper: wraps the function call.
import functools
def repeat(times):
"""Decorator factory to repeat a function call."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
Usage:
@repeat(3)
def say(message):
print(message)
say("Hello")
# Hello
# Hello
# Hello
Here, repeat(3) returns a decorator, which then wraps say.
Common real-world patterns
Timing a function
import functools
import time
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
Simple access control (role check)
import functools
def require_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if role not in user.get("roles", []):
raise PermissionError(f"User lacks role: {role}")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user, target_id):
print(f"User {target_id} deleted")
Caching results
You can write your own caching decorator, but Python also provides functools.lru_cache:
import functools
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Decorating methods
Decorators work the same way on functions and methods. When you decorate a method inside a class, the wrapper still receives self as the first argument:
import functools
def log_method(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
print(f"{self.__class__.__name__}.{func.__name__} called")
return func(self, *args, **kwargs)
return wrapper
class Greeter:
@log_method
def greet(self, name):
return f"Hello, {name}!"
Built-in decorators
Python includes several decorators you’ll see frequently:
@property— turn a method into a managed attribute@staticmethod— define a function stored on a class that doesn’t getselforcls@classmethod— method that receives the class (cls) instead of the instance
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("radius must be non-negative")
self._radius = value
When (and when not) to use decorators
Decorators are a good choice when:
- You see the same pattern repeated across many functions.
- You want to separate concerns (e.g., logging vs. business logic).
- You need reusable wrappers that can be applied with one line.
They can be overkill when:
- The behavior is used only once.
- The decorator logic is more complicated than the function it wraps.
- It makes the control flow harder to follow for your future self.
When in doubt, start with a normal helper function. If you notice a pattern repeating, refactor it into a decorator.