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:

PY manual.py
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"))
Output
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):

PY at_syntax.py
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"))
Output
HELLO BEN!

Keeping the original name: functools.wraps

PY wraps.py
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__)
Output
→ 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

PY timing.py
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))
Output
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:

PY retry.py
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())
Output
attempt 1 failed: timeout
attempt 2 failed: timeout
data

Built-in decorators

PY builtin.py
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)
Output
computing 9²
81
81
12.57 1
DecoratorPurpose
@functools.cache / @lru_cache(maxsize=…)Remember results for repeated inputs
@propertyMethod accessed like an attribute (lesson 39)
@staticmethod, @classmethodMethods that don't need an instance (lesson 39)
@dataclassGenerate class boilerplate (lesson 40)
@contextlib.contextmanagerTurn 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:

PY stacking.py
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())
Output
<b><i>hi</i></b>

Decorators in frameworks

PY frameworks.py
# 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.

Try it yourself

Try it yourself
Console output