Decorators and Callable Wrappers
A decorator receives an object and returns the object that should be bound to the decorated name. Function decorators commonly return a wrapper:
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def traced(function: Callable[P, R]) -> Callable[P, R]:
@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"calling {function.__qualname__}")
return function(*args, **kwargs)
return wrapper
functools.wraps preserves metadata and exposes __wrapped__, which matters to
documentation, introspection, debugging, and other decorators. The type
parameters preserve the wrapped callable's static signature.
Evaluation and composition
Decorator expressions are evaluated when the containing definition executes, usually during module import. Keep registration and configuration side effects predictable.
Stacked decorators apply from the inside out:
@outer
@inner
def operation(): ...
# Equivalent to: operation = outer(inner(operation))
Order therefore changes behavior for caching, retries, authentication, transactions, and logging.
Configured decorators
A decorator with arguments is a factory that returns the actual decorator:
def retry(*, attempts: int):
if attempts < 1:
raise ValueError("attempts must be positive")
def decorate(function):
@wraps(function)
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return function(*args, **kwargs)
except TransientError:
if attempt == attempts - 1:
raise
return wrapper
return decorate
Real retries also need backoff, jitter, idempotency, a narrow exception policy, and observability. A generic decorator cannot infer those domain rules.
Design boundaries
- Match the wrapped callable kind. A synchronous wrapper around an async function returns a coroutine without awaiting it; generators have similar lifecycle concerns.
- Do not hide important control flow, I/O, or broad exception suppression behind decorative syntax.
lru_cacherequires hashable arguments and is appropriate only when reusing a result is semantically safe. Mutable external state and time-dependent results violate that assumption.property,classmethod,staticmethod,contextmanager, andsingledispatchuse decorator syntax but define different contracts. Learn each API rather than treating every decorator as a wrapper.