Decorators
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;cacheis 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 awithblock (asynccontextmanagerfor async).enum.unique- underrated: reject anEnumthat 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.