Python 101Free
IDIOMATIC PYTHON

Decorators

Modifying a function by wrapping another function around it.

SECTION 01

A function wrapping a function

A decorator is a function that takes a function and returns a new function. Usually that new function does something extra (logging, timing, caching) and then calls the original.

The @decorator syntax above a def is just shorthand. @timer followed by def do_work(): is exactly the same as writing do_work = timer(do_work) after the definition. Python evaluates the decorator at definition time and replaces the name with whatever it returns.

Once you see decorators that way, the surprise wears off. They are not a special feature, they are an application of the rule that functions are first-class values. You can pass them around and wrap them in other functions, and decorators are the most common reason to do so.

python
import time

def timer(fn):
    def wrapper(*a, **kw):
        t = time.perf_counter()
        result = fn(*a, **kw)
        print(fn.__name__, time.perf_counter() - t)
        return result
    return wrapper

@timer
def slow():
    time.sleep(0.1)
SECTION 02

functools.wraps

By default, wrapping a function loses its name and docstring. The wrapper is technically a different function, so its __name__ becomes "wrapper" and its __doc__ becomes whatever you put on the wrapper, usually nothing.

functools.wraps fixes this. Apply @wraps(fn) to your inner wrapper, and Python copies the metadata (__name__, __doc__, __module__) from the original onto the wrapped version. Help text, debuggers, and introspection tools see the right name.

It costs one line of code and almost nothing at runtime, and the absence of it is a frequent source of "why does my decorated function show up as wrapper everywhere?" questions. Add it on every decorator you write.

python
from functools import wraps

def trace(fn):
    @wraps(fn)
    def wrapper(*a, **kw):
        return fn(*a, **kw)
    return wrapper

@trace
def greet(name):
    """Say hello."""
    return f"hi {name}"

greet.__name__   # 'greet' (not 'wrapper')
greet.__doc__    # 'Say hello.'
SECTION 03

Stacking decorators

You can apply multiple decorators to a single function. They stack from bottom to top: the one closest to the def runs first, wrapping the original. The next one up wraps that, and so on outward.

At call time the order reverses. The outermost decorator runs first, hands control to the next, and so on inward to the original. Then unwind in reverse, exiting the outermost last.

In practice this matters when decorators have observable order: a logger and an auth check, for example, want a defined order. Read the stack from bottom to top to see the call order.

python
@auth        # outer: runs first
@log         # inner: wraps the function directly
def handler(req):
    ...

# equivalent to:
handler = auth(log(handler))
← PREVIOUS
Iterators and Generators
NEXT →
Context Managers