Why does a decorated function report the wrapper's __name__ and __doc__, and how does functools.wraps fix it?
answer
- The name now points elsewhere
- Metadata lives on the object
- The inner def has its own dunders
- functools copies them for you
- It also sets __wrapped__
basics
~20 sDecoration rebinds the name to the wrapper, a different function object with its own metadata, so name, qualname and doc are the wrapper's. Applying functools.wraps to the wrapper copies that metadata from the wrapped function and sets wrapped.
solid answer
~40 sA decorator is just `f = deco(f)`. `deco` typically returns a brand-new function object — the wrapper — and every metadata attribute lives **on the object**, not on the name, so `f.__name__` becomes `'wrapper'`, `f.__doc__` becomes the wrapper's docstring (usually `None`), and `f.__qualname__` points at `deco.<locals>.wrapper`. That breaks `help()`, log lines that print `func.__name__`, and any registry keyed on the name. The fix is `functools.wraps(fn)` applied to the wrapper: it calls `functools.update_wrapper`, copying the names in `functools.WRAPPER_ASSIGNMENTS` (`__module__`, `__name__`, `__qualname__`, `__doc__`, and on 3.14 `__annotate__` plus `__type_params__`), merging the wrapped function's `__dict__` into the wrapper, and setting `__wrapped__` to the original so tools can unwrap the chain.
code
python · 16 linesimport functools
def timed(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@timed
def render(rows):
"Render the nightly report."
return len(rows)
print(render.__name__, render.__qualname__)
print(render.__doc__)
print(render.__wrapped__.__name__, render.__code__.co_varnames)go deeper
Recall that a decorator replaces the function object, and that functools.wraps is what you put on the inner wrapper so the decorated name still reports its own name and docstring.
Be ready to explain the rebinding, list the attributes the wrapper shadows, and describe what functools.wraps copies, including the dict merge and wrapped.
Show where the loss actually bites in production: logs keyed on func.name, generated docs, dispatch tables keyed on qualname, pickling by reference. Say clearly what wraps does not repair.
Own the convention: decorators in shared code must preserve introspection, because observability, docs tooling and registries all read these attributes. Decide whether a wrapper is even the right shape versus returning the same object.
### The rebinding, not the magic `@deco` above `def render(...)` is pure sugar for `render = deco(render)`. Nothing about that statement transfers identity: `deco` is handed the original function object, and whatever it returns is bound to the module-level name `render`. In the overwhelmingly common wrapper shape, what it returns is a **new** function object created by the inner `def wrapper(...)`, which was compiled from its own source and therefore carries its own metadata. That metadata is a set of ordinary attributes on the function object: - `__name__` — the bare name from the `def` statement, here `'wrapper'`. - `__qualname__` — the dotted path to the definition, here `'deco.<locals>.wrapper'`. - `__module__` — the module where the wrapper was defined, which is the *decorator's* module, not the decorated function's. - `__doc__` — the wrapper's docstring, normally `None`, so `help(render)` shows nothing useful. - `__dict__` — the wrapper's own (empty) attribute dict, so any custom attribute a previous decorator attached is gone. None of this is exotic: after decoration the name simply points at a different object, and you are reading that object's attributes. ### Why it actually hurts The damage is not cosmetic. Structured logging that emits `func.__name__` starts reporting every decorated call as `wrapper`. Documentation tooling and `help()` print the wrapper's empty docstring. Dispatch tables built from `fn.__qualname__` collapse, because dozens of unrelated decorated functions now share the qualname `deco.<locals>.wrapper`. Pickling a decorated module-level function by reference fails, because `pickle` locates it by `__module__` plus `__qualname__` and that path no longer resolves to this object. And error messages in tracebacks are less legible, because the code object's name is also `wrapper`. ### What functools.wraps actually does `functools.wraps(fn)` is a decorator factory: it returns `functools.partial(functools.update_wrapper, wrapped=fn)`, so writing `@functools.wraps(fn)` above `def wrapper` calls `update_wrapper(wrapper, fn)`. `update_wrapper` does three separate things: 1. **Assign** each attribute named in `functools.WRAPPER_ASSIGNMENTS` from the wrapped function onto the wrapper, skipping any the wrapped object lacks. On CPython 3.14 that tuple is `('__module__', '__name__', '__qualname__', '__doc__', '__annotate__', '__type_params__')` — `__annotate__` is the deferred-annotation slot introduced by PEP 649 in 3.14, and `__type_params__` arrived with PEP 695 in 3.12. 2. **Update** the wrapper's `__dict__` from the wrapped function's `__dict__`, per `functools.WRAPPER_UPDATES`, which is `('__dict__',)`. This is what carries custom attributes an inner decorator attached through the outer wrapper. 3. **Set** `wrapper.__wrapped__ = fn`, so introspection tools can walk back to the original. Those attributes are writable on a plain Python function, which is the only reason this works: `render.__name__ = 'x'` is legal. ### What it does not do `functools.wraps` copies **metadata**, not behaviour. The wrapper keeps its own `__code__`, so `wrapper.__code__.co_varnames` is still `('args', 'kwargs')`, and it keeps its own `__defaults__` and `__kwdefaults__` — the original's defaults are not transplanted. It does not make the wrapper accept the original's parameters; only the wrapper's actual `*args, **kwargs` code decides that. Introspection tools that report the original's parameters do so by *following* `__wrapped__`, not because the wrapper changed. Static type checkers likewise reason about the decorator's declared return type, not about what `wraps` copied at runtime. It is also worth knowing when the problem does not arise. A decorator that mutates and returns the *same* function object — a registration decorator, say, that records the function in a dict and returns it unchanged — loses nothing, because the name is rebound to the identical object. Class-based decorators (an instance with `__call__`) do not get `__name__` from `wraps` in the usual way either: you would call `functools.update_wrapper(self, fn)` in `__init__`, and the instance ends up with the copied attributes in its instance dict. ### Stacked decorators and the unwrap chain Decorators stack, and so does the metadata problem. With three layers each returning a wrapper, the outermost object is what the module name points at; if every layer used `functools.wraps`, each wrapper carries the innermost function's names and each holds a `__wrapped__` pointing one layer down, so the chain can be walked back to the original. If a single layer in the middle forgot `wraps`, the chain breaks there: names revert to `wrapper` for everything above it, and `__wrapped__` is absent on that layer, so nothing can recover the original programmatically. This is why the rule in shared code is not "use `wraps` where it matters" but "every wrapper uses `wraps`" — one careless layer poisons the whole stack, and the symptom shows up far from the cause, in a log line or a documentation build rather than at the decorator. A related trap is the decorator that returns something that is not a function at all — an instance of a callable class, or the result of `functools.lru_cache`. `functools.wraps` only assigns attributes the target object accepts, and an object built in C may reject them; check what you actually got before assuming the metadata landed. ### The interview shape The expected answer names the rebinding, names the attributes lost, names `functools.wraps` and — this is the discriminator — knows that `wraps` also merges `__dict__` and sets `__wrapped__`, and that it changes nothing about the wrapper's real code object or defaults.
- Besides the names, what else does functools.wraps transfer that a hand-written __name__ assignment misses?Two things. It merges the wrapped function's `__dict__` into the wrapper (`functools.WRAPPER_UPDATES` is `('__dict__',)`), so custom attributes attached by an inner decorator survive; and it sets `wrapper.__wrapped__` to the original, which is the hook introspection tools follow to recover the real function. Copying only `__name__` by hand gives you a nicer log line and nothing else.
- Does functools.wraps make the wrapper accept the original function's parameters?No. It copies metadata attributes only. The wrapper keeps its own `__code__`, `__defaults__` and `__kwdefaults__`, so it still accepts whatever its own `def` declares — usually `*args, **kwargs`. Tools that report the original parameters do it by following `__wrapped__` back to the wrapped function, not because anything about the wrapper changed.
- When does decoration cost you no metadata at all?When the decorator returns the same object it was given. A registration decorator that records the function in a dict and returns it unchanged rebinds the name to the identical object, so every attribute is untouched. `functools.wraps` is only needed when you return a new callable that stands in for the original.
Decoration is like forwarding a phone number to a new handset: callers still reach someone, but the caller ID, voicemail greeting and address book are the new handset's until you copy them across.
saying these in an interview costs you the question
- Thinks Python copies metadata to the wrapper automatically
- Believes __name__ is read-only on a function
- Claims functools.wraps changes the wrapper's real parameters
- Says help() reads the source file rather than __doc__
- Thinks functools.wraps is required for a decorator to work
- Cannot say what __wrapped__ is for