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.