datarekha
Python Medium Asked at StripeAsked at AirbnbAsked at Uber

Why is functools.wraps necessary when writing decorators?

The short answer

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:

AttributeWhy 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.

Learn it properly Decorators

Keep practising

All Python questions

Explore further

Skip to content