What does functools.wraps fix about a decorator's wrapper function?
answer
- The name now points somewhere else
- help() describes the inner function
- Identity lives on the function object
- One functools decorator copies it across
- WRAPPER_ASSIGNMENTS plus a __dict__ update
basics
~20 sfunctools.wraps copies the decorated function's identity onto the wrapper - its name, qualname, doc, module and the contents of dict - so help(), registries and documentation tooling describe the original function instead of an anonymous wrapper.
solid answer
~40 s`@deco` is just `f = deco(f)`, so the name ends up bound to the decorator's inner `wrapper` function, and identity attributes belong to that wrapper object. Without `functools.wraps`, `f.__name__` reads `'wrapper'`, `f.__qualname__` reads `'deco.<locals>.wrapper'`, `f.__doc__` is `None`, and `f.__module__` names the decorator's module. `help()` and doc builders then describe the wrapper, every function through the same decorator looks identical in a registry keyed by `__name__`, and `pickle` refuses the function because `__qualname__` no longer resolves to an importable object. `@functools.wraps(func)` on the wrapper calls `functools.update_wrapper(wrapper, func)`: it copies the names in `functools.WRAPPER_ASSIGNMENTS`, does a shallow `wrapper.__dict__.update(func.__dict__)`, and sets `wrapper.__wrapped__ = func` so introspection can still find the original.
code
python · 27 linesimport functools
def naive(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def careful(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@naive
def a(record):
"""Normalize one record."""
@careful
def b(record):
"""Normalize one record."""
print(a.__name__, a.__qualname__, a.__doc__)
# wrapper naive.<locals>.wrapper None
print(b.__name__, b.__qualname__, b.__doc__)
# b b Normalize one record.
print(hasattr(a, "__wrapped__"), b.__wrapped__.__name__)
# False bgo deeper
Be ready to write a two-line decorator and then say what help() prints for the decorated function. Recall the one-line fix - @functools.wraps(func) directly above the inner wrapper - and name the attributes it restores.
Explain that @functools.wraps(f) is just functools.update_wrapper(wrapper, f), name the WRAPPER_ASSIGNMENTS list, and point out that the dict handling is a shallow update rather than a replacement or a deep copy.
Show the consequences you have actually hit in production: registries and metric labels keyed by name collapsing to 'wrapper', a doc build rendering blank, and a pickling failure when a decorated callable crosses a process boundary.
Own the convention rather than the trivia: a codebase rule that every wrapper carries functools.wraps, enforced by a lint rule instead of review attention, and a decision about whether shared decorators live in one module so the rule has a single place to hold.
### Decoration rebinds a name; it does not rename an object `@deco` written above `def stage(...)` is pure sugar. Python compiles the `def`, builds a function object whose `__name__` is `"stage"`, calls `deco(stage)`, and binds the *result* back to the module global `stage`. Almost every decorator returns a brand-new inner function — conventionally called `wrapper` — so the name `stage` now points at that wrapper. Identity attributes belong to the function object that was compiled, not to the name it happens to be bound to. So after a naive decoration: * `stage.__name__` is `"wrapper"` * `stage.__qualname__` is `"deco.<locals>.wrapper"` * `stage.__doc__` is the wrapper's docstring, which is almost always `None` * `stage.__module__` names the module where the *decorator* was defined * `stage.__dict__` is empty, so any attribute hung on the original function is gone Nothing in the language notices or complains. The function still runs; it has simply lost its papers. ### Who actually reads those attributes The damage is not limited to `help()`, though `help(stage)` and `pydoc` output are the fastest way to see it. Concretely: * **Registries.** A decorator that does `REGISTRY[func.__name__] = func` collapses: every function decorated through the same decorator registers under `"wrapper"`, and the last one wins. * **Metrics and log messages** built from `func.__name__` by *another* decorator in the stack all report the same label, which makes a dashboard useless without any error being raised. * **Documentation tooling** reads `__doc__` and `__qualname__`; a decorated API renders blank. * **`pickle`** stores a plain function *by reference*: it writes `__module__` and `__qualname__` and re-imports the object on load. `"deco.<locals>.wrapper"` names a local object that no import can reach, so `pickle.dumps` raises `PicklingError`. That is why an un-wrapped decorator can break `multiprocessing` — the `spawn` and `forkserver` start methods pickle the callable, and since 3.14 `forkserver` is the default on Unix platforms other than macOS. * **`repr()`** of the function, which is what a traceback line and a debugger both show. ### What `functools.wraps` is `functools.wraps(func)` is a thin decorator factory: it returns a `functools.partial` of `functools.update_wrapper` with `wrapped=func` bound. So writing `@functools.wraps(func)` above the inner `wrapper` is exactly `functools.update_wrapper(wrapper, func)`, and `update_wrapper` does three things: 1. **Copies each name in `functools.WRAPPER_ASSIGNMENTS`** from the wrapped object onto the wrapper, silently skipping any the wrapped object does not have. On CPython 3.14 that tuple is `('__module__', '__name__', '__qualname__', '__doc__', '__annotate__', '__type_params__')`. 2. **Merges, for each name in `functools.WRAPPER_UPDATES`** — just `('__dict__',)` — with `wrapper.__dict__.update(func.__dict__)`. Note *update*, not replace, and note that it is **shallow**: a mutable attribute on the original is now the same object on both. 3. **Sets `wrapper.__wrapped__ = func`**, which is what lets `inspect` find the original later. The two version details worth knowing: `__type_params__` joined the tuple in 3.12 with PEP 695's type-parameter syntax, and in 3.14 PEP 649/749 made annotations lazy, so the tuple now carries `__annotate__` — the function that *computes* annotations — where 3.13 and earlier carried the already-built `__annotations__` dict. Reading `decorated.__annotations__` still gives you the original's annotations on every version; only the copied attribute changed. ### What it deliberately does not do `functools.wraps` is cosmetic in the precise sense that it never touches the wrapper's **code object**. The wrapper still really accepts `*args, **kwargs`, still appears as its own frame in a traceback, and is still a plain function even if the wrapped callable was a coroutine function. What changes is only what *introspection* reports — and `inspect.signature` reports the original's parameters because it follows `__wrapped__`, not because the wrapper's parameters changed. It also never raises for a missing attribute. Calling `functools.update_wrapper` against a `functools.partial` object, which has no `__name__`, quietly leaves the wrapper's own name in place and still sets `__wrapped__`. That silence is convenient and occasionally surprising. ### The practice Put `@functools.wraps(func)` on every wrapper you write, without deliberating about it. It is one line, it has no runtime cost after decoration, and the failures it prevents — a collapsed registry, a blank doc build, a pickling error at a process boundary — all surface far from the decorator that caused them. ### `wraps` and `update_wrapper` are the same operation `@functools.wraps(func)` is the decorator spelling and reads best directly above an inner `def`. `functools.update_wrapper(wrapper, func)` is the identical operation written as a plain call, which is what you reach for when there is no `def` in front of you: a callable you were handed, or one you have to fix up after it was built. Both accept optional `assigned` and `updated` arguments if you need to narrow or extend the two tuples - and note that even `assigned=(), updated=()` still sets `__wrapped__`, because that assignment is unconditional. ### How to demonstrate it in thirty seconds The whole answer fits in a REPL. Define one decorator without `functools.wraps` and one with it, apply each to a function with a docstring, and print `__name__`, `__qualname__` and `__doc__` for both. The first reports `wrapper`, `deco.<locals>.wrapper` and `None`; the second reports the original three. Then check `hasattr(decorated, "__wrapped__")` on each - `False` and `True` - which is the bridge to every introspection question that follows. Being able to produce that comparison from memory is what an interviewer is really checking, because it shows you have hit the problem rather than read the docstring.
- Which attributes does functools.wraps copy, and how does it treat the wrapper's __dict__?It copies every name in `functools.WRAPPER_ASSIGNMENTS` - on 3.14 that is `__module__`, `__name__`, `__qualname__`, `__doc__`, `__annotate__` and `__type_params__` - silently skipping any the wrapped object lacks. Then, for each name in `functools.WRAPPER_UPDATES`, it merges rather than replaces: `wrapper.__dict__.update(func.__dict__)`. That merge is shallow, so a mutable attribute hung on the original is now the same object on both, and mutating through either is visible through the other. Finally it sets `wrapper.__wrapped__ = func`.
- What does functools.wraps deliberately not fix?It never touches the wrapper's code object, so the wrapper genuinely still accepts `*args, **kwargs`; only introspection that follows `__wrapped__` reports the original parameters. It does not remove the wrapper's frame from a traceback, does not turn a plain wrapper into a coroutine function, and does not copy defaults or the closure. It also raises nothing when an attribute is missing - wrapping a `functools.partial`, which has no `__name__`, quietly leaves the wrapper's own name in place.
- Why can pickling a decorated module-level function fail when the wrapper is not wrapped?`pickle` stores a plain function by reference: it writes `__module__` and `__qualname__` and re-imports the object on load. An un-wrapped wrapper reports a `__qualname__` of `'deco.<locals>.wrapper'`, which names a local object no import can reach, so `pickle.dumps` raises `PicklingError`. Copying `__qualname__` across makes the lookup resolve again. This matters most with `multiprocessing` start methods that pickle the callable - `spawn`, and `forkserver`, which became the Unix default in 3.14.
Decorating is like putting a mail-forwarding service in front of an address. The letters still reach the right person, but the nameplate on the door now reads with the forwarder's name. functools.wraps repaints the nameplate with the original one.
saying these in an interview costs you the question
- Thinks functools.wraps changes which arguments the wrapper accepts
- Puts @functools.wraps on the decorator instead of the inner wrapper
- Claims a plain wrapper keeps __name__ and __doc__ automatically
- Dismisses lost metadata as cosmetic, ignoring registries and pickling
- Copies __name__ by hand and forgets __qualname__ and __doc__
- Believes functools.wraps hides the wrapper frame from tracebacks