Decorators

asyncpython

A decorator is a callable that takes a function (or class) and returns a replacement. @deco above a def is just sugar for func = deco(func). Examples checked with Python 3.13 (and, this being Python, themed after the films it is named for).

The basic shape

Always wrap with functools.wraps so the returned function keeps the original’s __name__, __doc__ and signature metadata:

import functools

def announce(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@announce
def quest(name):
    "state your quest"
    return f"{name} seeks the Holy Grail"

Without functools.wraps, quest.__name__ becomes "wrapper" and the docstring is lost, which breaks introspection, logging and some frameworks.

Decorators that take arguments

A decorator with arguments is a function that returns a decorator (two levels of nesting):

def retry(times):
    def deco(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last = None
            for _ in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as exc:
                    last = exc
            raise last
        return wrapper
    return deco

@retry(3)
def black_knight():
    "'Tis but a scratch! ... until it is not."
    ...

@retry(3) calls retry(3) first, which returns deco, which then wraps black_knight.

Stacking

Decorators apply bottom-up (closest to the function first):

@ni
@shrubbery
def knights():
    ...
# equivalent to knights = ni(shrubbery(knights))  -> shrubbery runs first, then ni

Handy standard-library decorators

The ones worth remembering, weighted toward the underrated:

# functools.total_ordering: define __eq__ and one of <, and get <=, >, >= for free
@functools.total_ordering
class Knight:
    def __init__(self, honour):
        self.honour = honour
    def __eq__(self, other):
        return self.honour == other.honour
    def __lt__(self, other):
        return self.honour < other.honour

# functools.singledispatch: pick an implementation by the first argument's type,
# instead of an isinstance ladder
@functools.singledispatch
def taunt(target):
    return "I fart in your general direction"

@taunt.register
def _(target: list):
    return "your mother was a hamster"
  • functools.wraps - preserve the wrapped function’s metadata (used above).
  • functools.cache / functools.lru_cache - memoize a pure function; cache is the unbounded shorthand.
  • functools.cached_property - compute an instance property once, then store it on the instance.
  • functools.total_ordering - underrated: fill in the missing comparison methods.
  • functools.singledispatch / singledispatchmethod - underrated: type-based dispatch.
  • contextlib.contextmanager - turn a generator into a with block (asynccontextmanager for async).
  • enum.unique - underrated: reject an Enum that has duplicate values.
  • atexit.register - underrated as a decorator: run a function when the interpreter exits.
  • dataclasses.dataclass - generate __init__, __repr__ and __eq__ from annotations.
  • abc.abstractmethod, typing.overload, typing.final - marks for ABCs and type checkers.
  • property, staticmethod, classmethod - the built-in method decorators.

Kinds of decorators

Andy Fundinger’s decorator taxonomy (EuroPython 2018) is a handy way to think about what a decorator is for. The kinds, with small examples:

Changing the arguments

Adjust or inject arguments before the call. Supplying an overridable default, for instance:

def with_default_swallow(func):
    @functools.wraps(func)
    def wrapper(*args, swallow="European", **kwargs):
        return func(*args, swallow=swallow, **kwargs)
    return wrapper

@with_default_swallow
def airspeed(swallow=None):
    return f"the airspeed of an unladen {swallow} swallow"

Controlling the call

Decide whether and how often the function runs. retry above is one, functools.cache caches, and run-once (a Holy Hand Grenade only goes off the once) is another:

def once(func):
    result = []
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if not result:
            result.append(func(*args, **kwargs))
        return result[0]
    return wrapper

Registering the object

Add the decorated function to a collection and return it unchanged. This is how plugin, command and dispatch registries are built:

round_table = {}

def knight(name):
    def deco(func):
        round_table[name] = func
        return func            # returned as-is, not wrapped
    return deco

@knight("Lancelot")
def charge():
    return "Charge!"

Binding to the class

Decorators that return a descriptor. property, classmethod and staticmethod are the everyday ones (see Descriptors).

Rewriting the object

Generate or rewrite code on the decorated object. @dataclass writes __init__, __repr__ and __eq__ from the annotations; at the far end, Cython and Numba rewrite a function for speed:

from dataclasses import dataclass

@dataclass
class Swallow:
    origin: str
    laden: bool

Recipes

Concrete, reusable snippets. Timing a call (how long is that silly walk?):

import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            print(f"{func.__name__} took {time.perf_counter() - start:.3f}s")
    return wrapper

More patterns will land here from real projects.

See also

  • Descriptors - the lower-level protocol behind property, methods and many decorators.

Related