C.W.K.
Stream
Lesson 02 of 05 · published

functools.wraps — Preserving Metadata

~15 min · wraps, functools, metadata, introspection

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The bug you'll write the first time

The naive decorator from lesson 1 has a problem: the wrapped function loses its name, docstring, and signature. hello.__name__ is now 'wrapper'. help(hello) shows the wrapper's docstring, not the original's. Tools that introspect functions — debuggers, doc generators, inspect.signature — see the wrapper, not the real function.

functools.wraps — copy the metadata over

The fix is one line: @functools.wraps(fn) on the wrapper. It copies __name__, __doc__, __module__, __qualname__, __annotations__, and __wrapped__ from the original function to the wrapper. Now help(hello) shows the original's docstring, and hello.__name__ is 'hello'.

__wrapped__ — getting at the original

One thing wraps adds is a __wrapped__ attribute pointing back to the original function. inspect.signature and inspect.unwrap use this. If you ever need to bypass the wrapper and call the original, wrapped_fn.__wrapped__() gets you there.

Principle: Always use @functools.wraps on your wrapper. There is no real downside, and the absence of it breaks introspection in ways that are sometimes hard to debug. Make it muscle memory.

What wraps does NOT preserve

The signature of the wrapper is still (*args, **kwargs) at the bytecode level. inspect.signature(hello) works thanks to __wrapped__, but the wrapper's actual __code__ still accepts anything. This is fine for most purposes; the cases where it matters (strict argument validation at the wrapper level) are rare.

Code

Without wraps — metadata lost·python
def announce(fn):
    def wrapper(*args, **kwargs):
        """This is the wrapper docstring."""
        print("calling", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

@announce
def hello(name):
    """Greet someone by name."""
    return f"hi {name}"

print(hello.__name__)        # 'wrapper'   <- lost
print(hello.__doc__)         # 'This is the wrapper docstring.'   <- lost
With functools.wraps — metadata preserved·python
import functools

def announce(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        print("calling", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

@announce
def hello(name):
    """Greet someone by name."""
    return f"hi {name}"

print(hello.__name__)        # 'hello'      ✓
print(hello.__doc__)         # 'Greet someone by name.'   ✓

# Bonus — __wrapped__ points to the original
print(hello.__wrapped__("pippa"))   # 'hi pippa'  — bypasses the wrapper
inspect.signature works correctly·python
import functools
import inspect

def trace(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

@trace
def compute(x: int, y: int = 5) -> int:
    """Return x + y."""
    return x + y

# Thanks to wraps and __wrapped__, signature is correct
print(inspect.signature(compute))    # (x: int, y: int = 5) -> int
print(compute.__doc__)               # 'Return x + y.'
print(compute.__annotations__)       # {'x': int, 'y': int, 'return': int}
The pattern — every decorator should look like this·python
import functools

def my_decorator(fn):
    @functools.wraps(fn)               # <- always this
    def wrapper(*args, **kwargs):
        # ... before-call logic ...
        result = fn(*args, **kwargs)
        # ... after-call logic ...
        return result
    return wrapper

External links

Exercise

Take the count_calls decorator from lesson 1 exercise, and add @functools.wraps. Show that (a) __name__ and __doc__ are preserved, (b) inspect.signature still works correctly, (c) wrapped_fn.__wrapped__ returns the original function. Then make a SECOND version that does NOT use wraps, and demonstrate the difference in repr() and help() output.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.