A decorator is a function that returns a function
args, *kwargs in the wrapper is not decoration — it is what lets one decorator work on functions with any signature. Without it, your decorator only fits the function you were looking at when you wrote it.
def shout(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return f"hi, {name}"
greet("ada") # 'HI, ADA'
greet = shout(greet) # this is literally what @shout does
Always use functools.wraps
Without wraps, every decorated function in your program reports itself as wrapper: in tracebacks, in logs, in help(), and to anything that inspects signatures. It is one line, it is never wrong, and forgetting it is the most common decorator bug.
from functools import wraps
def shout(fn):
@wraps(fn) # copies __name__, __doc__, __module__, __wrapped__
def wrapper(*args, **kwargs):
return fn(*args, **kwargs).upper()
return wrapper
greet.__name__ # 'greet' — without @wraps it would be 'wrapper'
A decorator that takes arguments
Three levels, because @retry(times=5) calls retry first and decorates with whatever comes back. That is the whole reason for the extra layer, and knowing it is why the shape stops looking arbitrary.
def retry(times=3): # the factory…
def decorator(fn): # …returns the decorator…
@wraps(fn)
def wrapper(*args, **kwargs): # …which returns the wrapper
last = None
for _ in range(times):
try:
return fn(*args, **kwargs)
except Exception as err:
last = err
raise last
return wrapper
return decorator
@retry(times=5)
def fetch(url): ...
Order, and state
Decorators apply from the bottom up. @cache under @log logs every call; @log under @cache logs only the misses. Which you want depends on what you are debugging, and getting it backwards is a puzzling afternoon.
A decorator can keep state — a call count, a cache, a set of seen arguments — in its closure, and expose it by attaching an attribute to the wrapper: wrapper.calls = 0.
@log # applied second — the outer wrapper
@cache # applied first — the inner one
def work(n): ...
# work = log(cache(work)) — bottom-up, like the mathematics
Where you have already met them
That last pattern — a decorator whose real job is to register the function somewhere — is how most web frameworks, CLI libraries and plugin systems work.
| Decorator | Does |
|---|---|
@property | Method, accessed like an attribute |
@staticmethod / @classmethod | Changes what the first argument is |
@functools.cache | Memoises on the arguments |
@dataclass | Writes __init__, __repr__, __eq__ |
@app.route("/") | Registers the function in a table — a decorator with a side effect |