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'sFix:
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 accessibleWhat @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) ← liesWith @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
# equivalent to @wraps but for instances
functools.update_wrapper(self, func)
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 decoratorUseful 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 4
- 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 Decorator with arguments, Decorators and Context Managers in Python, Introspection in Python.