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.
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__)
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⟩>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__)
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⟩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__)
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⟩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"))
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⟩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⟩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: Nonedef 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] greetdef wrapper(*args, **kwargs):
16@wraps(func)17def wrapper(*args('Alice',), **kwargs):18 result = func(*args('Alice',), **kwargs{})19 return result.upper()def greet(name):
25@uppercase26def greet(nameAlice):27 """Greet a person"""28 return f"hello, {nameAlice}"result ← hello, Alice
17def wrapper(*args, **kwargs):18 result→ hello, Alice = func(*args('Alice',), **kwargs{})19 return resulthello, Alice.upper()print(greet("Alice"))
35# Call36print(greet("Alice"))outputHELLO, ALICE
The __wrapped__ Attribute
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))
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⟩x ← 2
20# Call decorated21x→ 2 = 222#@x=5, 1023print(add(x2, 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 adddef add(a, b):
pass 1 of 215@trace16def add(a2, b3):17 return a2 + b3original ← ⟨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))output5def add(a, b):
pass 2 of 215@trace16def add(a10, b20):17 return a10 + b20print("calling original directly:", original(10, 20))
26original = add.__wrapped__27print("calling original directly:", original(10, 20))outputcalling original directly: 30
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⟩x ← 5
20# Call decorated21x→ 5 = 522print(add(x5, 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 adddef add(a, b):
pass 1 of 215@trace16def add(a5, b3):17 return a5 + b3original ← ⟨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))output8def add(a, b):
pass 2 of 215@trace16def add(a10, b20):17 return a10 + b20print("calling original directly:", original(10, 20))
25original = add.__wrapped__26print("calling original directly:", original(10, 20))outputcalling original directly: 30
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⟩x ← 10
20# Call decorated21x→ 10 = 1022print(add(x10, 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 adddef add(a, b):
pass 1 of 215@trace16def add(a10, b3):17 return a10 + b3original ← ⟨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))output13def add(a, b):
pass 2 of 215@trace16def add(a10, b20):17 return a10 + b20print("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