A decorator replaces a function object with a different function object. Every
piece of metadata that pointed at the original now points at the wrapper —
which is why an undecorated wrapper turns help(fn) into “wrapper(args,
*kwargs)”, breaks pydoc, breaks any framework that routes on
fn.__name__, and breaks inspect.signature.
functools.wraps copies the metadata across. The full list it handles:
| Attribute | Why it matters |
|---|---|
__name__ |
logging, error messages, framework routing |
__qualname__ |
tracebacks and repr |
__doc__ |
help(), pydoc, doctests |
__module__ |
pickling by reference, pydoc |
__dict__ |
attributes other decorators attached (updated, not replaced) |
__annotations__ |
anything reading types at runtime |
__type_params__ |
PEP 695 type parameters — added to the list in 3.12 |
__wrapped__ |
set to the original, so introspection can unwrap |
The first six are copied (__dict__ is updated, so several stacked
decorators accumulate rather than clobber). __wrapped__ is the one wraps
adds, and it is the one that makes the rest work properly:
inspect.signature follows __wrapped__ by default and reports the
original signature, not the wrapper’s (*args, **kwargs).
from functools import wraps
def logged(fn: Callable[P, R]) -> Callable[P, R]:
@wraps(fn)
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
return fn(*args, **kwargs)
return inner
💡inspect.signature follows __wrapped__. When is that the wrong
click to reveal
answer?
When the decorator genuinely changes the signature. A decorator that
injects a dependency (@inject_db turning f(db, x) into f(x)) or one that
adds a parameter reports the pre-decoration signature, which is now a lie —
and frameworks that build call sites from signature() (dependency injectors,
CLI generators, web routers, pytest fixtures) will construct the wrong call.
Two ways out. Pass follow_wrapped=False to inspect.signature at the point
where you want the wrapper’s real shape. Or, in the decorator, set
inner.__signature__ explicitly to the signature you actually expose —
signature() prefers __signature__ over unwrapping.
The general rule: use wraps when the wrapper is signature-preserving (which
is most of the time), and take responsibility for __signature__ when it is
not.
What ParamSpec adds
wraps fixes runtime introspection. It does nothing for the type checker —
Callable[..., Any] in and out means every call site of the decorated
function stops being checked, and the return type degrades to Any, which
then propagates into the caller.
ParamSpec (PEP 612) fixes the static half:
P = ParamSpec("P")
R = TypeVar("R")
def logged(fn: Callable[P, R]) -> Callable[P, R]: ...
P captures the entire parameter list — positional, keyword, defaults,
*args, **kwargs — and replays it on the wrapper, so calls to the decorated
function are checked exactly as if the decorator were not there.
The two are complementary and you want both on essentially every decorator:
wraps for the runtime metadata, ParamSpec for the static signature. See
item 2.11 for the full treatment, including Concatenate for decorators that
add or remove leading parameters.
💡Your decorator adds a keyword-only retries parameter to the
click to reveal
functions it wraps. Callable[P, R] -> Callable[P, R] is now wrong. What
does the type system offer?
Not much, and it is worth knowing where the edge is. Concatenate handles
parameters added or removed at the front, positionally:
def with_db(fn: Callable[Concatenate[Connection, P], R]) -> Callable[P, R]: ...
That is exactly right for a decorator that supplies a leading argument. There
is no equivalent for adding a keyword-only parameter — PEP 612 has no
syntax for “P plus one more keyword”, and the usual workaround is an
explicit Protocol with a __call__ that spells out the new shape, which
gives up genericity over the wrapped signature.
The practical consequence is a design nudge: decorators that add keyword
options are hard to type, so prefer configuring the decorator itself
(@retry(times=3)) over having it inject an option into the wrapped
function’s call signature. The typeable design is usually also the clearer
one.