Decorators power caching, logging, retries, auth checks and most framework APIs (Flask, pytest, FastAPI).
A decorator is a callable that takes a function and returns a replacement. The @ syntax is pure sugar:
@timed
def load(): ...
# is the same as
load = timed(load)The usual implementation is a closure: the decorator defines an inner wrapper(*args, **kwargs) that does something before and/or after calling the original, then returns wrapper. Accepting *args, **kwargs lets it wrap any signature.
Always decorate the wrapper with @functools.wraps(func). Without it, the decorated function's __name__, __doc__ and __module__ become the wrapper's, which breaks debugging, logging, help() and tools that introspect functions. wraps also sets __wrapped__, giving access to the original.
A decorator that takes arguments, like @retry(times=3), needs one more level: retry(times=3) is called first and must return the actual decorator. Three nested functions — factory, decorator, wrapper — look intimidating but follow mechanically.
Decorators stack: the one nearest the def is applied first. They also work on classes, receiving the class object (that's how @dataclass works), and on methods.
The standard library's most useful decorators:
functools.cache/lru_cache(maxsize=...)memoise pure functionsproperty,classmethod,staticmethod,functools.cached_propertycontextlib.contextmanager,functools.singledispatch
Decorators run at definition time (import time), not call time — keep that work cheap and free of side effects.
Example
import functools
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f"attempt {attempt} failed: {e}")
raise RuntimeError(f"{func.__name__} failed {times} times")
return wrapper
return decorator
outcomes = iter([ValueError("flaky"), ValueError("flaky"), "ok"])
@retry(times=3, exceptions=(ValueError,))
def fetch():
"""Fetch something unreliable."""
result = next(outcomes)
if isinstance(result, Exception):
raise result
return result
print(fetch()) # two failures, then: ok
print(fetch.__name__, fetch.__doc__) # fetch Fetch something unreliable.Remember
@dabovedef fmeansf = d(f)- Always use
functools.wrapsin wrappers - Decorators with arguments are factories that return a decorator
Watch out for
- Forgetting
returnin the wrapper, so the decorated function always returns None.