Decorators
A decorator wraps a function to add behaviour — logging, timing, caching, access checks, retrying — without changing the function's own code. The @decorator syntax above a definition is used throughout Python frameworks, from Flask routes to pytest fixtures.
Functions that wrap functions
A decorator is just a function that takes a function and returns a new one:
def shout(fn): def wrapper(*args, **kwargs): result = fn(*args, **kwargs) return result.upper() + "!" return wrapper def greet(name): return f"hello {name}" loud_greet = shout(greet) print(loud_greet("ana"))
HELLO ANA!
wrapper accepts any arguments (*args, **kwargs), calls the original function, and adds something to the result.
The @ syntax
Writing @shout above a definition is shorthand for greet = shout(greet):
def shout(fn): def wrapper(*args, **kwargs): return fn(*args, **kwargs).upper() + "!" return wrapper @shout def greet(name): return f"hello {name}" print(greet("ben"))
HELLO BEN!
Keeping the original name: functools.wraps
import functools def logged(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): print(f"→ {fn.__name__}{args}") result = fn(*args, **kwargs) print(f"← {fn.__name__} returned {result!r}") return result return wrapper @logged def add(a, b): """Add two numbers.""" return a + b add(2, 3) print(add.__name__, "-", add.__doc__)
→ add(2, 3) ← add returned 5 add - Add two numbers.
Without @functools.wraps(fn), the decorated function would be called wrapper and lose its docstring, which confuses debuggers, documentation tools and other decorators. Always include it.
A timing decorator
import functools import time def timed(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): start = time.perf_counter() try: return fn(*args, **kwargs) finally: elapsed = time.perf_counter() - start print(f"{fn.__name__} took {'under' if elapsed < 0.5 else 'over'} 0.5s") return wrapper @timed def build_report(rows): return sum(i * i for i in range(rows)) print(build_report(10_000))
build_report took under 0.5s 333283335000
Decorators with arguments
To configure a decorator, add one more layer: a function that takes the options and returns the actual decorator:
import functools def retry(times): def decorator(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): for attempt in range(1, times + 1): try: return fn(*args, **kwargs) except ConnectionError as err: print(f"attempt {attempt} failed: {err}") raise ConnectionError(f"gave up after {times} attempts") return wrapper return decorator calls = 0 @retry(times=3) def fetch(): global calls calls += 1 if calls < 3: raise ConnectionError("timeout") return "data" print(fetch())
attempt 1 failed: timeout attempt 2 failed: timeout data
Built-in decorators
from functools import cache @cache def slow_square(n): print(f"computing {n}²") return n * n print(slow_square(9)) print(slow_square(9)) class Circle: def __init__(self, radius): self.radius = radius @property def area(self): return round(3.14159 * self.radius ** 2, 2) @staticmethod def unit(): return Circle(1) print(Circle(2).area, Circle.unit().radius)
computing 9² 81 81 12.57 1
| Decorator | Purpose |
|---|---|
@functools.cache / @lru_cache(maxsize=…) | Remember results for repeated inputs |
@property | Method accessed like an attribute (lesson 39) |
@staticmethod, @classmethod | Methods that don't need an instance (lesson 39) |
@dataclass | Generate class boilerplate (lesson 40) |
@contextlib.contextmanager | Turn a generator into a with context manager |
Stacking decorators
Several decorators can be applied to one function. They're applied bottom-up — the one closest to def wraps first:
def bold(fn): return lambda: f"<b>{fn()}</b>" def italic(fn): return lambda: f"<i>{fn()}</i>" @bold @italic def text(): return "hi" print(text())
<b><i>hi</i></b>
Decorators in frameworks
# Flask: register a URL route @app.route("/users/<int:user_id>") def show_user(user_id): ... # pytest: provide test data @pytest.fixture def client(): ... # Click: build a command-line interface @click.command() @click.option("--count", default=1) def hello(count): ...
These look like magic at first, but they're the same idea: a function that receives your function and registers or wraps it.