Your decorated functions show up in stack traces as "wrapper" instead of their real names, and help() shows no documentation. functools.wraps preserves the original function's identity, making your decorators production-ready.

The wrapped Attribute

problem.py
# Problem without wraps


def without_wraps(func):
    def wrapper():
        return func()

    return wrapper


@without_wraps
def greet():
    """Say hello"""
    return "hello"


# Check metadata
print("name:", greet.__name__)
print("doc:", greet.__doc__)

solution.py
# Solution with wraps

from functools import wraps


def with_wraps(func):
    @wraps(func)
    def wrapper():
        return func()

    return wrapper


@with_wraps
def greet():
    """Say hello"""
    return "hello"


# Check metadata
print("name:", greet.__name__)
print("doc:", greet.__doc__)

attributes.py
# Preserved attributes

from functools import wraps


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

    return wrapper


@decorator
def sample(x: int, y: str) -> str:
    """Sample function with annotations"""
    return f"{x}: {y}"


# Check preserved attributes
print("name:", sample.__name__)
print("doc:", sample.__doc__)
print("module:", sample.__module__)
print("annotations:", sample.__annotations__)

stacked.py
# Stacked decorators

from functools import wraps


def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[LOG] {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


def uppercase(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()

    return wrapper


@log
@uppercase
def greet(name):
    """Greet a person"""
    return f"hello, {name}"


# Check metadata (should be greet, not wrapper)
print("name:", greet.__name__)
print("doc:", greet.__doc__)

# Call
print(greet("Alice"))

wrapped.py
# Accessing __wrapped__

from functools import wraps


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


@trace
def add(a, b):
    return a + b


# Call decorated
x = 
print(add(x, 3))

# Access original via __wrapped__
original = add.__wrapped__
print("calling original directly:", original(10, 20))

# Accessing __wrapped__

from functools import wraps


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


@trace
def add(a, b):
    return a + b


# Call decorated
x = 
print(add(x, 3))

# Access original via __wrapped__
original = add.__wrapped__
print("calling original directly:", original(10, 20))

# Accessing __wrapped__

from functools import wraps


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


@trace
def add(a, b):
    return a + b


# Call decorated
x = 
print(add(x, 3))

# Access original via __wrapped__
original = add.__wrapped__
print("calling original directly:", original(10, 20))

Why It Matters

  • Debugging: stack traces show correct function names
  • Documentation: help() shows the original docstring
  • Introspection: tools can access the original signature
metadata loss - without @wraps, a decorator's wrapper shadows the original function's name and docstring
functools.wraps - a decorator that copies metadata from the wrapped function to the wrapper

Exercise: practical.py

Fix a broken decorator by adding proper wraps usage