Overview
A decorator is a function that takes another function and extends its behavior without modifying its source. They appear everywhere in Python: @property, @staticmethod, @app.route in Flask, @pytest.fixture in tests. This tutorial explains how they work and how to write your own.
Functions Are Objects
To understand decorators, first remember that in Python, functions are first-class objects.
def greet(name):
return f"Hello, {name}"
say_hi = greet
print(say_hi("Alice")) # Hello, Alice
def apply(func, value):
return func(value)
print(apply(greet, "Bob")) # Hello, Bob
Your First Decorator
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # HELLO, ALICE
The @uppercase line is syntactic sugar for:
greet = uppercase(greet)
Preserving Metadata with functools.wraps
Without care, the decorated function loses its name and docstring:
print(greet.__name__) # wrapper — wrong
Fix it with functools.wraps:
import functools
def uppercase(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
print(greet.__name__) # greet
Always use functools.wraps. It preserves introspection, which matters for debugging, documentation, and frameworks that rely on function metadata.
Decorators with Arguments
To accept parameters, add another layer:
def repeat(times):
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
@repeat(3)
def say(msg):
print(msg)
say("hello")
The call chain is: repeat(3) returns decorator, which receives say and returns wrapper.
Practical Use Case 1: Timing
import time
import functools
def timed(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
@timed
def slow_task():
time.sleep(1)
slow_task() # slow_task took 1.0012s
Practical Use Case 2: Retry on Failure
def retry(attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except Exception as err:
last_error = err
print(f"Attempt {attempt} failed: {err}")
time.sleep(delay)
raise last_error
return wrapper
return decorator
@retry(attempts=3, delay=0.5)
def fetch_data():
# may raise a network error
...
Practical Use Case 3: Authentication in Flask
from functools import wraps
from flask import request, jsonify
def require_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
if not token or not verify_token(token):
return jsonify({"error": "Unauthorized"}), 401
return func(*args, **kwargs)
return wrapper
@app.route("/profile")
@require_auth
def profile():
return jsonify({"name": "Alice"})
Practical Use Case 4: Caching
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Python's standard library already provides functools.lru_cache and functools.cache, which are faster and thread-safe.
Stacking Decorators
@timed
@retry(attempts=2)
def fetch():
...
Decorators apply bottom-up: retry wraps fetch first, then timed wraps the retried function.
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} of {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print("hi")
Class-based decorators are useful when you need to keep state across calls.
Common Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
Missing functools.wraps |
Function name becomes wrapper |
Add the decorator |
| Mutable default in wrapper | State leaks between calls | Store state inside a closure or class |
| Decorator without parentheses when args are expected | TypeError at definition time |
Use the three-level form |
| Decorating async functions with a sync wrapper | Coroutine is not awaited | Write an async wrapper with await |
