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.

functools.wraps is a decorator for decorator wrappers that preserves the original function's metadata (__name__, __doc__, __module__, __annotations__, etc.).

The Problem

Without @wraps, a decorator's wrapper function shadows the original function's metadata:

problem.py
Replay: real traced execution (multi-file project)
# 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__)

  1. def without_wraps(func):

    4def without_wraps(func⟨function greet A⟩):5    def wrapper():6        return func()78    return wrapper<function without_wraps.<locals>.wrapper at ⟨addr B⟩>
  2. print("name:", greet.__name__)

    17# Check metadata18print("name:", greet.__name__wrapper)19print("doc:", greet.__doc__None)
    outputname: wrapper
    doc: None
metadata loss - without @wraps, a decorator's wrapper shadows the original function's name and docstring

The Solution

Use @wraps(func) on the wrapper:

solution.py
Replay: real traced execution (multi-file project)
# 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__)

  1. def with_wraps(func):

    6def with_wraps(func⟨function greet A⟩):7    @wraps(func)8    def wrapper():9        return func()1011    return wrapper⟨function greet B⟩
  2. print("name:", greet.__name__)

    20# Check metadata21print("name:", greet.__name__greet)22print("doc:", greet.__doc__None)
    outputname: greet
    doc: None

Now __name__, __doc__, etc. are preserved.

functools.wraps - a decorator that copies metadata from the wrapped function to the wrapper

Preserved Attributes

attributes.py
Replay: real traced execution (multi-file project)
# 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__)

  1. def decorator(func):

    6def decorator(func⟨function sample A⟩):7    @wraps(func)8    def wrapper(*args, **kwargs):9        return func(*args, **kwargs)1011    return wrapper⟨function sample B⟩
  2. print("name:", sample.__name__)

    20# Check preserved attributes21print("name:", sample.__name__sample)22print("doc:", sample.__doc__None)23print("module:", sample.__module____main__)24print("annotations:", sample.__annotations__{'x': <class 'int'>, 'y': <class 'str'>, 'return': <class 'str'>})
    outputname: sample
    doc: None
    module: __main__
    annotations: {'x': <class 'int'>, 'y': <class 'str'>, 'return': <class 'str'>}

Stacked Decorators

stacked.py
Replay: real traced execution (multi-file project)
# 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"))

  1. def uppercase(func):

    15def uppercase(func⟨function greet A⟩):16    @wraps(func)17    def wrapper(*args, **kwargs):18        result = func(*args, **kwargs)19        return result.upper()2021    return wrapper⟨function greet B⟩
  2. def log(func):

    6def log(func⟨function greet B⟩):7    @wraps(func)8    def wrapper(*args, **kwargs):9        print(f"[LOG] {func.__name__}")10        return func(*args, **kwargs)1112    return wrapper⟨function greet C⟩
  3. print("name:", greet.__name__)

    31# Check metadata (should be greet, not wrapper)32print("name:", greet.__name__greet)33print("doc:", greet.__doc__None)3435# Call36print(greet("Alice"))
    outputname: greet
    doc: None
  4. def wrapper(*args, **kwargs):

    7@wraps(func)8def wrapper(*args('Alice',), **kwargs):9    print(f"[LOG] {func.__name__greet}")10    return func(*args('Alice',), **kwargs{})
    output[LOG] greet
  5. def wrapper(*args, **kwargs):

    16@wraps(func)17def wrapper(*args('Alice',), **kwargs):18    result = func(*args('Alice',), **kwargs{})19    return result.upper()
  6. def greet(name):

    25@uppercase26def greet(nameAlice):27    """Greet a person"""28    return f"hello, {nameAlice}"
  7. result ← hello, Alice

    17def wrapper(*args, **kwargs):18    result→ hello, Alice = func(*args('Alice',), **kwargs{})19    return resulthello, Alice.upper()
  8. print(greet("Alice"))

    35# Call36print(greet("Alice"))
    outputHELLO, ALICE

The __wrapped__ Attribute

x
wrapped.py
Replay: real traced execution (multi-file project)
# 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 = 2
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 = 5
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 = 10
print(add(x, 3))

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

  1. def trace(func):

    6def trace(func⟨function add A⟩):7    @wraps(func)8    def wrapper(*args, **kwargs):9        print(f"calling {func.__name__}")10        return func(*args, **kwargs)1112    return wrapper⟨function add B⟩
  2. x ← 2

    20# Call decorated21x→ 2 = 222#@x=5, 1023print(add(x2, 3))
  3. def wrapper(*args, **kwargs):

    7@wraps(func)8def wrapper(*args(2, 3), **kwargs):9    print(f"calling {func.__name__add}")10    return func(*args(2, 3), **kwargs{})
    outputcalling add
  4. def add(a, b):

    pass 1 of 2
    15@trace16def add(a2, b3):17    return a2 + b3
  5. original ← ⟨function add A⟩

    22#@x=5, 1023print(add(x2, 3))2425# Access original via __wrapped__26original→ ⟨function add A⟩ = add.__wrapped__⟨function add A⟩27print("calling original directly:", original(10, 20))
    output5
  6. def add(a, b):

    pass 2 of 2
    15@trace16def add(a10, b20):17    return a10 + b20
  7. print("calling original directly:", original(10, 20))

    26original = add.__wrapped__27print("calling original directly:", original(10, 20))
    outputcalling original directly: 30
  1. def trace(func):

    6def trace(func⟨function add A⟩):7    @wraps(func)8    def wrapper(*args, **kwargs):9        print(f"calling {func.__name__}")10        return func(*args, **kwargs)1112    return wrapper⟨function add B⟩
  2. x ← 5

    20# Call decorated21x→ 5 = 522print(add(x5, 3))
  3. def wrapper(*args, **kwargs):

    7@wraps(func)8def wrapper(*args(5, 3), **kwargs):9    print(f"calling {func.__name__add}")10    return func(*args(5, 3), **kwargs{})
    outputcalling add
  4. def add(a, b):

    pass 1 of 2
    15@trace16def add(a5, b3):17    return a5 + b3
  5. original ← ⟨function add A⟩

    21x = 522print(add(x5, 3))2324# Access original via __wrapped__25original→ ⟨function add A⟩ = add.__wrapped__⟨function add A⟩26print("calling original directly:", original(10, 20))
    output8
  6. def add(a, b):

    pass 2 of 2
    15@trace16def add(a10, b20):17    return a10 + b20
  7. print("calling original directly:", original(10, 20))

    25original = add.__wrapped__26print("calling original directly:", original(10, 20))
    outputcalling original directly: 30
  1. def trace(func):

    6def trace(func⟨function add A⟩):7    @wraps(func)8    def wrapper(*args, **kwargs):9        print(f"calling {func.__name__}")10        return func(*args, **kwargs)1112    return wrapper⟨function add B⟩
  2. x ← 10

    20# Call decorated21x→ 10 = 1022print(add(x10, 3))
  3. def wrapper(*args, **kwargs):

    7@wraps(func)8def wrapper(*args(10, 3), **kwargs):9    print(f"calling {func.__name__add}")10    return func(*args(10, 3), **kwargs{})
    outputcalling add
  4. def add(a, b):

    pass 1 of 2
    15@trace16def add(a10, b3):17    return a10 + b3
  5. original ← ⟨function add A⟩

    21x = 1022print(add(x10, 3))2324# Access original via __wrapped__25original→ ⟨function add A⟩ = add.__wrapped__⟨function add A⟩26print("calling original directly:", original(10, 20))
    output13
  6. def add(a, b):

    pass 2 of 2
    15@trace16def add(a10, b20):17    return a10 + b20
  7. print("calling original directly:", original(10, 20))

    25original = add.__wrapped__26print("calling original directly:", original(10, 20))
    outputcalling original directly: 30

Why It Matters

  • Debugging: stack traces show correct function names
  • Documentation: help() shows the original docstring
  • Introspection: tools can access the original signature

Exercise: practical.py

Fix a broken decorator by adding proper wraps usage