Python Guide
Python Decorators Explained
The most elegant way to add functionality to functions without touching their code.
Quick answer: A decorator is a function that takes another function as an argument, wraps it in a new function, and returns the wrapper. Use the @decorator syntax above a function to apply it. Use functools.wraps inside your wrapper to preserve the original function's metadata.
Functions Are First-Class Objects
Before understanding decorators, you must understand that in Python, functions can be passed around like any other value:
def greet(name):
return f"Hello, {name}!"
# Assign to a variable
say_hi = greet
print(say_hi("Alice")) # Hello, Alice!
# Pass as argument
def run(func, arg):
return func(arg)
print(run(greet, "Bob")) # Hello, Bob!
This is what makes decorators possible.
Your First Decorator
Here is a decorator that times how long a function takes to run:
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "done"
slow_function()
# Output: slow_function took 1.0021s
The @timer syntax is equivalent to writing slow_function = timer(slow_function).
Why Use functools.wraps
Without @wraps, the decorated function loses its identity:
# Without @wraps
def bad_decorator(func):
def wrapper():
return func()
return wrapper
@bad_decorator
def my_func():
"""My docstring"""
pass
print(my_func.__name__) # 'wrapper' ❌
print(my_func.__doc__) # None ❌
# With @wraps
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper():
return func()
return wrapper
@good_decorator
def my_func():
"""My docstring"""
pass
print(my_func.__name__) # 'my_func' ✅
print(my_func.__doc__) # 'My docstring' ✅
Practical Decorators
Logging decorator:
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3)
# Calling add with (2, 3) {}
Retry decorator:
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:
print(f"Attempt {attempt + 1} failed: {e}")
raise Exception("All retries failed")
return wrapper
return decorator
@retry(times=5)
def unstable_api_call():
# Will be retried 5 times on failure
pass
Stacking Decorators
You can apply multiple decorators to one function. They run bottom-up:
@timer
@log_calls
def process_data():
return "result"
# Equivalent to:
# process_data = timer(log_calls(process_data))
#
# log_calls runs first, then timer
💡 Remember: The decorator closest to the function wraps first. If you want logging to time the log-then-run process, order matters.
Decorators with Arguments
To pass arguments to your decorator, wrap it in another function:
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_hello():
print("Hello!")
say_hello()
# Hello!
# Hello!
# Hello!
🛡️ Best Practices
- Always use
@functools.wrapsin your wrapper function. - Accept
*args, **kwargsin the wrapper so it works with any signature. - Return the result of
func(*args, **kwargs)— don't forget the return. - Keep decorators focused on one responsibility (one concern per decorator).
- Use decorators for cross-cutting concerns, not business logic.
- Document what your decorator does — readers won't see the wrapper code.
Common mistake: Forgetting to return func(*args, **kwargs) inside the wrapper. This silently returns None from every decorated call — a bug that is hard to debug.
❓ Frequently Asked Questions
What is a decorator in Python?
A decorator is a function that takes another function as an argument and returns a modified version of it. Decorators let you add functionality to existing functions without changing their code.
When should I use decorators in Python?
Use decorators for cross-cutting concerns like logging, timing, caching, authentication, and validation. They keep your core logic clean by separating concerns.
What does functools.wraps do?
functools.wraps copies the metadata (name, docstring, annotations) of the wrapped function onto the wrapper. Without it, the decorated function loses its identity.
Can I use multiple decorators on one function?
Yes. Stack decorators on top of each other. They are applied bottom-up: the decorator closest to the function is applied first.
What is the difference between a decorator and a wrapper?
A decorator is the outer function that takes a function as input. A wrapper is the inner function that actually wraps the original function's behavior. Every decorator contains a wrapper.