Python Decorators: A Practical Guide
Understand how Python decorators work under the hood, build your own, and learn the patterns used in Flask, Django, and pytest.
What Is a Decorator?
A decorator is a function that takes another function as an argument, extends its behavior, and returns a new function — all without modifying the original function’s source code. The @ syntax is just syntactic sugar.
# These are equivalent:
@my_decorator
def my_function():
pass
# Same as:
def my_function():
pass
my_function = my_decorator(my_function)
Building Your First Decorator
import functools
import time
def timer(func):
@functools.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)
slow_function() # "slow_function took 1.0012s"
Why functools.wraps?
Without @functools.wraps(func), the wrapper function replaces the original function’s name and docstring. The wraps decorator preserves the original function’s metadata — essential for debugging and documentation.
Decorators with Arguments
To pass arguments to a decorator, you need an extra layer of nesting:
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("World") # Prints 3 times
Class-Based Decorators
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Call #{self.count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # Call #1 n Hello!
say_hello() # Call #2 n Hello!
Real-World Patterns
Caching / Memoization
Python’s built-in @functools.lru_cache is a decorator that caches function results:
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Authentication (Flask-style)
def login_required(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not current_user.is_authenticated:
return redirect("/login")
return func(*args, **kwargs)
return wrapper
Stacking Decorators
Decorators are applied bottom-up (closest to the function first):
@decorator_a
@decorator_b
def my_func():
pass
# Equivalent to: decorator_a(decorator_b(my_func))
Summary
Decorators are one of Python's most elegant features. They follow the open/closed principle — extending behavior without modifying existing code. Once you understand the wrapping pattern, you will see decorators everywhere in the Python ecosystem.