Python decorators
A decorator is a function transformer. The craft is transforming behavior without destroying the function's identity: name, docstring, signature, type hints.
Method
- Always apply functools.wraps. Without it,
help(), tracebacks, pickling, and any framework that inspects__name__or__doc__see the wrapper, not the function.@functools.wraps(fn)on the inner wrapper is line one of every decorator. - Keep the wrapper signature generic and forward everything.
def wrapper(*args, **kwargs)thenreturn fn(*args, **kwargs). Dropping the return value is the most common decorator bug; the second is swallowing exceptions meant for the caller. - Parameterize with a factory.
@retry(times=3)meansretryis a function returning a decorator: three nested defs. If you want both@retryand@retry(times=3)to work, detect the single-callable-arg case explicitly; document which form you support. - Preserve types for checkers. Annotate with ParamSpec:
def deco(fn: Callable[P, R]) -> Callable[P, R]. Without it, every decorated function degrades toAnyand the type checker goes blind downstream. - Decide state placement. Per-call state lives in the wrapper's
locals; cross-call state (counters, caches) belongs in a closure
variable or attribute on the wrapper, and needs a lock if threads call
it. For stdlib cases use
functools.lru_cache/cacheinstead of rolling your own. - Class decorators transform the class, not instances. They run once
at definition: registering the class, injecting methods, wrapping
selected callables from
vars(cls). For per-instance behavior use__init_subclass__or a metaclass only as a last resort. - Support async when callers need it. A sync wrapper around a
coroutine function returns an un-awaited coroutine and "works" until
nothing runs. Branch on
inspect.iscoroutinefunction(fn)and provide an async wrapper path.
Boundaries
- If the behavior needs configuration at call time rather than definition time, a plain higher-order function call is clearer than a decorator.
- Stacked decorators execute bottom-up; ordering-sensitive stacks (auth before cache before retry) deserve a comment stating the order.
- Do not use decorators to mutate global registries at import time in libraries; imports become side effects the user cannot control.