Why is functools.wraps necessary when writing decorators?
Without functools.wraps, the wrapper function replaces the original's name, doc, module, qualname, and annotations, breaking introspection, logging, and documentation tools. functools.wraps copies all of these attributes from the wrapped function to the wrapper and stores a reference in wrapped for unwrapping.
How to think about it
Decorators are everywhere in Python — Flask routes, pytest fixtures, framework plumbing — so this question isn’t really about whether you can write one. It’s about the production pitfall almost everyone hits once: without @functools.wraps, the wrapper silently steals the wrapped function’s identity, and suddenly logging, routing, test discovery, and docs all start misbehaving.
The problem: identity theft
A naive wrapper overwrites the original’s metadata, because the name now points at wrapper:
def timer(func):
def wrapper(*args, **kwargs): # no @wraps
return func(*args, **kwargs)
return wrapper
@timer
def train(epochs):
"""Train the model for a given number of epochs."""
...
print(train.__name__) # 'wrapper' — wrong
print(train.__doc__) # None — the docstring is gone
That one slip breaks Flask/FastAPI route naming, pytest fixture discovery, Sphinx docs, and any log line that prints func.__name__.
The fix
@functools.wraps(func) copies the wrapped function’s identity onto the wrapper. Here it preserves everything, including __wrapped__, which points back at the original:
import functools
def timed(func):
@functools.wraps(func) # copies __name__, __doc__, __annotations__, …
def wrapper(*args, **kwargs):
print(f" [{func.__name__}] called")
return func(*args, **kwargs)
return wrapper
@timed
def train(epochs: int) -> int:
"""Train the model for a given number of epochs."""
return sum(range(epochs))
print("__name__ :", train.__name__) # 'train'
print("__doc__ :", train.__doc__)
print("__wrapped__:", train.__wrapped__.__name__) # the original function
result = train(1000)
print("result :", result)
import inspect
print("unwrapped :", inspect.unwrap(train).__name__)
__name__ : train
__doc__ : Train the model for a given number of epochs.
__wrapped__: train
[train] called
result : 499500
unwrapped : train
What it copies
@functools.wraps(func) is shorthand for functools.update_wrapper(wrapper, func), which carries across:
| Attribute | Why it matters |
|---|---|
__name__ | Flask/FastAPI routes, logging, pytest fixtures |
__doc__ | Sphinx, help(), IDE hover docs |
__module__ | Correct module attribution |
__qualname__ | Accurate stack traces |
__annotations__ | Type checkers, FastAPI request parsing |
__wrapped__ | Lets inspect.unwrap peel back the layers |
The idea underneath
__wrapped__ is the quietly important one. It lets inspect.unwrap walk an entire decorator stack back to the original function — which is how testing tools recover the real signature, and how debuggers show meaningful frames instead of a tower of wrappers.