functools.wraps

Updated 2 min read source
On this page8
  1. The gotcha
  2. Minimal repro
  3. What @wraps copies
  4. What breaks without @wraps
  5. class-based decorators — update_wrapper
  6. Recovering the original via __wrapped__
  7. Custom assignments
  8. Interview angle

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

python
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:

python
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 to func so 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:

python
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):

python
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:

python
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__

python
@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:

python
@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.wraps and 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.