Writing decorators with wraps

functools · Practical · 9 min

Without wraps, decorators break help(), logging, debugging tools and frameworks that inspect functions.

A decorator replaces a function with a wrapper. Without help, the wrapper's metadata is the wrapper's own: __name__ becomes "wrapper", the docstring disappears, and inspect.signature shows (*args, **kwargs). That breaks help(), error messages, logging that prints function names, and frameworks like FastAPI or pytest that inspect signatures.

@functools.wraps(func) on the inner function copies the metadata over:

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ...
        return func(*args, **kwargs)
    return wrapper

It copies __module__, __name__, __qualname__, __doc__, __annotations__ and __type_params__, merges the wrapper's __dict__ with the original's, and sets `__wrapped__` pointing to the original function. inspect.signature follows __wrapped__, so the real signature shows up, and you can reach the undecorated function via f.__wrapped__ in tests.

Decorators with arguments need one more level of nesting: an outer function takes the arguments and returns the actual decorator.

Make it a habit: every function-wrapping decorator you write should use @wraps.

Example

import inspect
import time
from functools import wraps

def timed(label):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            try:
                return func(*args, **kwargs)
            finally:
                print(f"[{label}] {func.__name__} took {time.perf_counter() - start:.0f}s")
        return wrapper
    return decorator

@timed("db")
def fetch(user_id: int, *, fresh: bool = False) -> dict:
    """Load a user record."""
    return {"id": user_id}

print(fetch(7))                 # [db] fetch took 0s  then {'id': 7}
print(fetch.__name__, fetch.__doc__)   # fetch Load a user record.
print(inspect.signature(fetch))        # (user_id: int, *, fresh: bool = False) -> dict
print(fetch.__wrapped__(8))            # {'id': 8}  (undecorated)

Remember

  • Use @wraps(func) on every wrapper function
  • It copies name, docstring and annotations and sets __wrapped__
  • Decorators with arguments add an outer factory function

Watch out for

  • Writing decorators without wraps, so every decorated function appears as 'wrapper' in logs and tracebacks.

Check yourself

  1. What does inspect.signature(f) show for a function decorated with a wrapper(*args, **kwargs) that uses @wraps?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/functools.html#functools.wraps