Decorators
functools.wraps
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