functools.wraps
The gotcha
A decorator that doesn’t use @functools.wraps(func) replaces the function’s metadata with the wrapper’s. __name__, __doc__, __module__, __qualname__, __wrapped__, __dict__ — all silently overwritten. Logging, debugging, introspection, Sphinx docs, and unittest.mock all break.
Minimal repro
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
"""Adds two numbers."""
return a + b
print(add.__name__) # "wrapper" ← lost original name
print(add.__doc__) # None ← lost docstring
help(add) # shows wrapper's signature, not add's
Fix:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
"""Adds two numbers."""
return a + b
print(add.__name__) # "add"
print(add.__doc__) # "Adds two numbers."
add.__wrapped__ # <function add at ...> — original accessible
What @wraps copies
By default, functools.wraps(func) copies these attributes from func to the wrapper:
__module____name____qualname____doc____dict__(the function’s own attribute dict)__wrapped__— set tofuncso you can recover the original
Equivalent to functools.update_wrapper(wrapper, func, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES).
What breaks without @wraps
Tracebacks show “wrapper” instead of the actual function name — debugging nightmare in stack traces full of decorated functions.
inspect.signature returns (*args, **kwargs) (the wrapper’s signature) instead of the real one:
import inspect
def deco(f):
def wrapper(*args, **kwargs): return f(*args, **kwargs)
return wrapper
@deco
def real(x: int, y: str): pass
inspect.signature(real) # (*args, **kwargs) ← lies
With @wraps(f):
inspect.signature(real) # (x: int, y: str)
unittest.mock.patch.object(..., autospec=True) uses inspect.signature — so without @wraps, autospec ends up with the wrapper’s permissive *args, **kwargs signature and won’t catch wrong-arg-count bugs.
Sphinx / pdoc documentation generators read __doc__ and __qualname__ — without @wraps, your decorated functions have empty docs.
Type checkers (mypy, pyright) often special-case @functools.wraps to keep the original signature visible.
class-based decorators — update_wrapper
For decorators that wrap with a callable object instead of a function:
import functools
class CountedCalls:
def __init__(self, func):
self.func = func
self.count = 0
functools.update_wrapper(self, func) # equivalent to @wraps but for instances
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountedCalls
def hello():
"""says hello"""
print("hi")
print(hello.__name__) # "hello"
print(hello.__doc__) # "says hello"
@functools.wraps is sugar for update_wrapper; update_wrapper works on any target including class instances.
Recovering the original via __wrapped__
@log_calls
def add(a, b): return a + b
original = add.__wrapped__
original(1, 2) # bypasses the decorator
Useful in tests when you want to call the undecorated function directly without the side effects (logging, retry, caching).
Custom assignments
Need to copy more attributes? Pass assigned:
@functools.wraps(func, assigned=('__name__', '__doc__', '__custom__'))
def wrapper(*args, **kwargs): ...
Or pass updated=('__dict__',) to merge instead of overwrite (default).
Interview angle
- Q: “What’s
functools.wrapsand why is it needed?” — copies the wrapped function’s metadata to the wrapper, so introspection / debugging / docs still work. - Q: “What breaks without it?” —
__name__,__doc__,inspect.signature, autospec mocks, Sphinx, debugger frames. - Follow-up: “How do you recover the original function from a decorated one?” —
decorated.__wrapped__. - Follow-up: “How do you do this for a class-based decorator?” —
functools.update_wrapper(self, func)in__init__.
See 29_decorator_with_arguments.md, 12_decorators_context_managers.md, 04_introspection.md.