Decorators
A decorator is a callable that takes a function (or class) and returns a replacement. @deco above a def is sugar for func = deco(func).
A bit of a magic feel to it, sometimes, so understand the mechanism, then be deliberate. The name is all the reader gets, so think twice before using one: is it readable, and does it bring more than the decorator-free version? For real?
The basic shape
Wrap with functools.wraps. It copies __module__, __name__, __qualname__, __doc__, __type_params__ and the annotations from the wrapped function, updates the wrapper’s __dict__, and sets __wrapped__ to the original.
import functools
def announce(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@announce
def quest(name: str, swallow: str = "European") -> str:
"state your quest"
return f"{name} seeks the Holy Grail"
Without it, quest.__name__ becomes "wrapper" and the docstring is lost. The __wrapped__ link is the part that matters beyond cosmetics, because anything built on inspect.signature follows it back to the real parameter list. Decorate a pytest test without wraps and the fixtures stop arriving:
TypeError: test_without_wraps() missing 1 required positional argument: 'value'
pytest saw (*args, **kwargs), concluded the test requested no fixtures, and injected none. The identical test with wraps passes.
A decorator that takes arguments is a function returning a decorator - one more level of nesting, so @retry(3) calls retry(3) first and applies the result.
What wraps does not copy is the signature. inspect.signature looks correct only because it follows __wrapped__ by default:
>>> inspect.signature(quest)
(name: str, swallow: str = 'European') -> str
>>> inspect.signature(quest, follow_wrapped=False)
(*args, **kwargs) -> str
The -> str on the second line is not real either: wraps copied the annotations wholesale onto a wrapper that accepts anything. Static type checkers see (*args, **kwargs), which is what ParamSpec exists to fix.
That annotations entry changed in Python 3.14: functools.WRAPPER_ASSIGNMENTS now carries __annotate__ rather than __annotations__, so the wrapper inherits the wrapped function’s deferred annotation machinery instead of an eagerly built dict (PEP 649 and PEP 749). Reading wrapper.__annotations__ still returns the same mapping, so nothing breaks unless you inspect that tuple yourself. Note that the 3.14 documentation for update_wrapper has not caught up - it still lists __annotations__, while Lib/functools.py on the same branch has said __annotate__ since 3.14.
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
The order stops being academic as soon as one decorator stamps an attribute rather than wrapping. A marking decorator applied above @cached_property stamps the cached_property object, which is what getattr on the class returns; applied below, it stamps the underlying function, where nothing inspecting the class will find it:
class Above:
@mark("above")
@functools.cached_property
def value(self): ...
class Below:
@functools.cached_property
@mark("below")
def value(self): ...
>>> Above.__dict__["value"].__mark__
'above'
>>> Below.__dict__["value"].__mark__
AttributeError: 'cached_property' object has no attribute '__mark__'
It landed on .func instead, where nothing walking the class will look for it.
Kinds of decorators
Andy Fundinger’s decorator taxonomy (EuroPython 2018) is a useful way to think about what a decorator is for. Each kind already has canonical examples in the standard library, which is the fastest way to recognise one in the wild.
- Changing the arguments. Adjust or inject arguments before the call - supplying an overridable default, injecting a connection or a lock. The wrapper’s signature differs from the wrapped one, which is exactly the case
Concatenatewas designed for. - Controlling the call. Decide whether, when and how often the function runs:
functools.cacheandfunctools.lru_cacheskip the call, a retry decorator repeats it, a run-once decorator suppresses it after the first time. - Registering the object. Add the function to a collection and return it unchanged:
atexit.register,functools.singledispatch.register, pytest fixtures, Flask and FastAPI route decorators. Nothing is wrapped, so nothing changes at the call site - the effect is entirely elsewhere. - Binding to the class. Return a descriptor.
property,classmethodandstaticmethodare the everyday ones; see Descriptors. - Rewriting the object. Generate or rewrite code on the decorated object.
dataclasses.dataclasswrites__init__,__repr__and__eq__from the annotations,enum.uniqueinspects and rejects, and at the far end Cython and Numba rewrite a function for speed.
Worth adding to that list: marking, a variant of registering that stamps an attribute instead of filling a collection, so a later pass can find the object by scanning. It also returns the object untouched, so no wraps is needed and identity is preserved. Two things to know: f.__marker__ = value fails on __slots__ classes and on C-level builtins, and validation happens whenever the marks are read rather than at decoration time, so a typo surfaces far from the line that caused it.
Registering and marking are the two least visible kinds. Neither changes the object, so the only evidence either ever ran is the decorator line itself. Registration also runs at import time, which makes it silently dependent on the module being imported at all - the usual reason a handler “is not registered” is that nothing imported it. Registries are worth calling out as the dominant architectural use of decorators in application code, most often to break an import cycle between infrastructure and the modules it dispatches to.
The name is the contract
A decorator can do anything. Return the function untouched, return a wrapper, return a different function, return a descriptor, or return something that is not callable at all:
def replace_with_a_potato(func):
return Potato()
That is a legal decorator, and under that name it is an honest one: you read the line and you know. Call the identical code @foo and it becomes unreadable, with no amount of care at the call site able to recover it.
This is the real difficulty in using decorators well. Everywhere else in Python the reader can follow along - a call shows its arguments, an assignment shows its value. @foo shows nothing at all. The name is the entire interface, and the reader either trusts it or goes and reads the decorator.
So, the same test as anywhere magic is involved: is the cost lower or higher than the developer experience it buys? For decorators that cost is concentrated almost entirely in the name. Name it for what it does to the function, not for the concept behind it. @retry, @cached, @requires_login and @register all survive being read cold by someone who has never seen the codebase. @smart, @handler and @process do not.
Failure modes
Four, all of them consequences of the mechanism rather than mistakes in the decorator.
@deco and @deco() are not interchangeable. A decorator that takes arguments is a factory, so applying it bare passes the decorated function in as its first argument. Nothing raises at that point - the function is quietly replaced by the inner decorator, and the failure surfaces later, somewhere else:
@joined # the () is missing
def lines():
yield "a"
yield "b"
lines is now the inner apply function rather than the wrapper, so calling it returns another function instead of a string. The classic fix is a callable() guard that accepts both forms:
def dual(arg=None):
if callable(arg): # applied bare: arg is the decorated function
return dual()(arg)
def apply(f):
...
return f
return apply
Mixing the two conventions across a codebase is a readability cost in itself, since nothing at the call site says which decorators need the parentheses.
A synchronous decorator around async def measures nothing. Calling a coroutine function returns a coroutine object; the body has not run yet. A timing wrapper therefore times object creation:
took 0.000001s
result: the Holy Grail | real duration 0.2s
The await still works, so nothing visibly fails - the numbers are simply wrong. Anything that wraps the call rather than the result needs an async def wrapper, usually alongside the synchronous one.
A class used as a decorator does not bind as a method. Decorate a method with a class that implements __call__ but not __get__, and self never arrives:
TypeError: Knight.charge() missing 1 required positional argument: 'self'
This is not a decorator problem, it is the descriptor protocol: an attribute that does not define __get__ is returned as-is, so nothing binds the instance. A function-based decorator works because functions are non-data descriptors, and __get__ is what produces the bound method. Give the class a __get__, or use a function. See Descriptors.
Closure state is shared. State in the enclosing scope belongs to the decoration, which happens once, at import. On a decorated method that means one counter or one cache for every instance of the class - the same trap as storing state on a descriptor.
Universal decorator recipe
Two axes have to be handled to accept everything: the call shape (@deco or @deco(...)) and the callable kind (plain function, coroutine function, generator function, async generator function). Both are bounded problems with a known answer.
First, check you need one. A @contextlib.contextmanager object is already a ContextDecorator, so it works as a decorator with no extra code, and it recreates the context on every call. On an async function it applies just as cleanly, and is silently wrong:
@timer("slow")
async def slow():
await asyncio.sleep(0.2)
return "done"
enter slow
exit slow after 0.000s
body starts
body ends
The context opened and closed before the body ever ran, because the wrapper is synchronous and calling a coroutine function only builds a coroutine. Nothing raises. That failure is the entire reason for the dispatch below.
Both call shapes, without guessing. Make the options keyword-only. The first positional parameter can then only ever be the decorated function, so the callable() guard from the previous section becomes unnecessary:
def timed(func=None, *, label=None):
if func is None: # @timed(...) - configured, function comes later
return functools.partial(timed, label=label)
@timed passes the function positionally. @timed(label="x") leaves func as None, returns a partial, and that receives the function on the next call. The callable() heuristic is only needed when you insist on positional options, and it misfires as soon as an option is itself a callable.
Both worlds, without duplicating the logic. The wrapper bodies must differ - await, yield from and async for are not interchangeable - but the cross-cutting logic should exist once. Put it in a context manager and have every branch use it:
import functools
import inspect
import time
from contextlib import contextmanager
@contextmanager
def _timing(name):
"""The cross-cutting logic, written once for all four branches."""
start = time.perf_counter()
try:
yield
finally:
print(f"{name} took {time.perf_counter() - start:.3f}s")
def timed(func=None, *, label=None):
if func is None:
return functools.partial(timed, label=label)
if isinstance(func, type):
raise TypeError("timed decorates functions, not classes")
name = label or func.__name__
if inspect.isasyncgenfunction(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
with _timing(name):
async for item in func(*args, **kwargs):
yield item
elif inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
with _timing(name):
return await func(*args, **kwargs)
elif inspect.isgeneratorfunction(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
with _timing(name):
yield from func(*args, **kwargs)
else:
@functools.wraps(func)
def wrapper(*args, **kwargs):
with _timing(name):
return func(*args, **kwargs)
return wrapper
Checked on 3.12, 3.13 and 3.14: all three call shapes on both sync and async, generators and async generators, plain methods, and inspect.iscoroutinefunction(wrapper) still True afterwards so the next decorator up can detect it. The timing now genuinely spans the await.
What it does not survive.
- Classes. Wrapping a class in a function replaces it -
isinstance(Thing, type)becomesFalseand every subclass andisinstancecheck downstream breaks. Hence the explicit guard. @staticmethodand@classmethodmust stay outermost. Below them, class access appears to work and instance access does not:TypeError: Bad.oops() takes 0 positional arguments but 1 was given.- Detection through a stack. If an inner decorator already wrapped a coroutine function in a synchronous wrapper,
iscoroutinefunctionreportsFalseand you get the sync branch - the exact bug you were avoiding. Since Python 3.12 the inner decorator can declare itself withinspect.markcoroutinefunction, which fixes detection for everything above it. Callinginspect.unwrapbefore testing is the other option, and it is wrong whenever an inner decorator deliberately turned an async function into a sync one. - Use
inspect.iscoroutinefunction, not theasyncioone - it sees throughfunctools.partial, andasyncio.iscoroutinefunctionraises aDeprecationWarningfrom 3.14.
Four branches, a context manager and a sentinel argument, to make one @ accept every shape. By this page’s own argument that has to earn its place: worth it for a decorator applied across a codebase, not for one used three times in a single module, where the two lines you actually need are clearer than the machinery.
Note also what it should not be called. @universal names the machinery; @timed names what happens to the function.
Typing a decorator
Getting a decorator past a type checker is the part the tutorials skip. Callable[..., T] discards every parameter type, so the decorated function ends up accepting anything.
ParamSpec and Concatenate (Python 3.10, PEP 612) forward the parameters instead:
from collections.abc import Callable
def add_logging[T, **P](f: Callable[P, T]) -> Callable[P, T]:
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
return f(*args, **kwargs)
return inner
Concatenate covers the decorator that adds or removes a parameter - injecting a lock, a session, a request - where the visible signature is the wrapped one minus the injected argument.
The [T, **P] syntax above is PEP 695, Python 3.12. Before that, declare P = ParamSpec("P") at module level.
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.
See also
- Descriptors - the lower-level protocol behind
property, method binding and the class-decorator trap above. - PEP 318 - the original decorator proposal, including the syntax alternatives that were rejected.