When you decorate a function in Python, the original function's name, docstring, module, and type annotations are replaced by those of the wrapper function. This breaks help(), logging, debugging tools, documentation generators, and any code that inspects __name__ or __doc__. The fix is one line: @functools.wraps(func). This article explains exactly what gets lost, why, what functools.wraps does about it, and the edge cases you should know.
Every Python function carries metadata in special attributes: __name__ holds the function name, __doc__ holds the docstring, __module__ identifies where the function was defined, __qualname__ provides the fully qualified name, and __annotations__ stores type hints. When a decorator replaces the function with a wrapper, all of these attributes belong to the wrapper unless you explicitly transfer them. This article walks through the problem, the standard solution, and the deeper mechanics of how Python's introspection tools interact with decorated functions.
The Problem: Metadata Loss
Consider a simple logging decorator applied to a function with a docstring and type annotations:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def calculate_area(radius: float) -> float:
"""Calculate the area of a circle given its radius."""
return 3.14159 * radius ** 2
After decoration, check what Python sees:
print(calculate_area.__name__) # wrapper
print(calculate_area.__doc__) # None
print(calculate_area.__annotations__) # {}
print(calculate_area.__module__) # __main__ (this one happens to match)
The name is wrapper instead of calculate_area. The docstring is gone. The type annotations are gone. The function's identity has been erased. The name calculate_area in the namespace now points to the wrapper function object, and that object has no knowledge of the original function's metadata.
Running help(calculate_area) produces output that describes a function called wrapper with no documentation and a generic (*args, **kwargs) signature -- unhelpful for anyone trying to understand the API.
The Fix: functools.wraps
The standard library provides functools.wraps specifically for this problem. It is a decorator applied to the wrapper function inside your decorator. It copies the original function's metadata onto the wrapper in a single line:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def calculate_area(radius: float) -> float:
"""Calculate the area of a circle given its radius."""
return 3.14159 * radius ** 2
Now the metadata is preserved:
print(calculate_area.__name__) # calculate_area
print(calculate_area.__doc__) # Calculate the area of a circle given its radius.
print(calculate_area.__annotations__) # {'radius': <class 'float'>, 'return': <class 'float'>}
One line -- @functools.wraps(func) -- restores the function's identity. The wrapper still runs the pre-call and post-call code, but it now presents itself to the outside world as the original function.
What functools.wraps Copies
Understanding what functools.wraps transfers makes it clear why it solves the problem. By default, it performs two operations: it assigns specific attributes from the original function directly onto the wrapper, and it updates the wrapper's __dict__ with the contents of the original function's __dict__.
The attributes copied by default are defined in the constant functools.WRAPPER_ASSIGNMENTS:
| Attribute | What It Contains |
|---|---|
__module__ |
The name of the module where the function is defined |
__name__ |
The function's short name (e.g., calculate_area) |
__qualname__ |
The fully qualified name, including class context (e.g., MyClass.method) |
__doc__ |
The docstring |
__annotations__ |
Type hints declared in the function signature |
In addition to these five assigned attributes, functools.wraps merges the original function's __dict__ into the wrapper's __dict__. This means any custom attributes that were set on the original function (like markers, flags, or metadata added by other decorators) carry over to the wrapper.
Finally, functools.wraps sets one additional attribute: __wrapped__, which holds a direct reference to the original function.
The __wrapped__ Attribute
The __wrapped__ attribute is set automatically by functools.wraps and points back to the original unwrapped function. This provides a way to bypass the decorator entirely, which is useful for testing, debugging, and introspection:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
"""Add two numbers."""
return a + b
# Call through the decorator (logging active)
add(3, 5) # Calling add -> 8
# Bypass the decorator entirely
add.__wrapped__(3, 5) # 8 (no log output)
In unit tests, you can use function.__wrapped__ to test the core logic of a decorated function without triggering side effects from decorators like logging, timing, authentication, or retry logic.
How inspect.signature Follows __wrapped__
Even with functools.wraps, the wrapper's actual code-level signature is still (*args, **kwargs). The wrapper catches all arguments generically and forwards them to the original function. So why does inspect.signature() show the correct parameter names?
The answer is __wrapped__. By default, inspect.signature() follows the __wrapped__ attribute chain until it finds a function without a __wrapped__ attribute, then returns that function's signature:
import inspect
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import time
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
return result
return wrapper
@timer
def multiply(x: int, y: int, factor: float = 1.0) -> float:
"""Multiply x by y, optionally scaled by factor."""
return x * y * factor
print(inspect.signature(multiply))
# (x: int, y: int, factor: float = 1.0) -> float
Without functools.wraps, inspect.signature() would return (*args, **kwargs) -- losing all parameter names, types, and default values. This would break IDE autocompletion, documentation generators like Sphinx, and help() output.
You can disable this behavior by passing follow_wrapped=False to inspect.signature(), which returns the wrapper's own signature instead of following the chain:
print(inspect.signature(multiply, follow_wrapped=False))
# (*args, **kwargs)
Using functools.wraps in Parameterized Decorators
Parameterized decorators -- decorators that accept their own arguments -- have three layers of nesting. The @functools.wraps(func) line always goes on the innermost function, the one that replaces the original function:
import functools
def retry(max_tries=3):
def decorator(func):
@functools.wraps(func) # Always on the innermost wrapper
def wrapper(*args, **kwargs):
for attempt in range(1, max_tries + 1):
try:
return func(*args, **kwargs)
except Exception:
if attempt == max_tries:
raise
return wrapper
return decorator
@retry(max_tries=5)
def fetch_data(url: str) -> dict:
"""Fetch JSON data from the given URL."""
import requests
return requests.get(url).json()
print(fetch_data.__name__) # fetch_data
print(fetch_data.__doc__) # Fetch JSON data from the given URL.
A common mistake is placing @functools.wraps on the decorator function (the middle layer) instead of the wrapper function (the innermost layer). The decorator receives the function, but it is the wrapper that replaces it. The @wraps must go on whatever function gets returned to the namespace.
What Breaks Without functools.wraps
help() and Documentation Generators
The built-in help() function reads __name__, __doc__, and the function signature. Without functools.wraps, help(calculate_area) displays documentation for wrapper(*args, **kwargs) with no docstring. Sphinx, pdoc, and other documentation generators produce the same incorrect output because they use inspect.signature() under the hood.
Logging and Error Messages
Logging decorators that use func.__name__ internally still work because they capture the reference at decoration time. But external logging that reads __name__ from the decorated function will see wrapper for every decorated function, making log entries impossible to distinguish.
Serialization and Registration
Frameworks that register functions by name -- such as Flask's route registry, Celery's task registry, or signal dispatch systems -- rely on __name__ and __qualname__ to identify and look up functions. If every decorated function reports its name as wrapper, registration collisions and lookup failures occur.
Testing and Debugging
Stack traces show function names using __name__ and __qualname__. Without preserved metadata, every decorated function appears as wrapper in tracebacks, making it significantly harder to locate the source of an error.
functools.update_wrapper for Advanced Control
functools.wraps is a convenience wrapper around functools.update_wrapper. When you need finer control -- for example, to copy additional attributes or to skip certain default attributes -- you can call update_wrapper directly:
import functools
EXTENDED_ASSIGNMENTS = (
'__module__', '__name__', '__qualname__',
'__doc__', '__annotations__', '__defaults__', '__kwdefaults__'
)
def strict_metadata(func):
"""Decorator that copies extended metadata including defaults."""
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
functools.update_wrapper(
wrapper, func,
assigned=EXTENDED_ASSIGNMENTS
)
return wrapper
@strict_metadata
def greet(name: str, greeting: str = "Hello") -> str:
"""Return a personalized greeting."""
return f"{greeting}, {name}!"
print(greet.__name__) # greet
print(greet.__defaults__) # ('Hello',)
print(greet.__doc__) # Return a personalized greeting.
The assigned parameter accepts a tuple of attribute names to copy directly from the original function. The updated parameter (defaulting to ('__dict__',)) specifies which attributes to merge rather than replace. This gives you full control over what metadata travels from the original function to the wrapper.
The relationship between the two functions is simple: @functools.wraps(func) is equivalent to functools.update_wrapper(wrapper, func) called manually after the wrapper definition. For the vast majority of decorators, @functools.wraps(func) is sufficient. Use update_wrapper directly only when you need to customize the assignment or update tuples.
Key Takeaways
- Decorators replace the original function with a wrapper. The wrapper has its own
__name__,__doc__,__module__,__qualname__, and__annotations__. Without intervention, the original metadata is lost. - functools.wraps copies five attributes and sets __wrapped__. Applying
@functools.wraps(func)to the wrapper copies__module__,__name__,__qualname__,__doc__, and__annotations__. It also merges__dict__and sets__wrapped__to reference the original function. - inspect.signature follows the __wrapped__ chain. This is how
help(), IDE autocompletion, and documentation generators display the correct parameter names and defaults even though the wrapper's own signature is(*args, **kwargs). - __wrapped__ enables bypass for testing. Calling
func.__wrapped__(args)invokes the original function directly, skipping all decorator logic. This is invaluable for unit testing the core function without side effects from logging, timing, or retry decorators. - Every decorator you write should include @functools.wraps(func). It costs one line and one import. Omitting it breaks
help(), logging, serialization, framework registration, stack traces, and documentation generation. No decorator is too simple to warrant skipping it. - In parameterized decorators, @wraps goes on the innermost function. The wrapper that replaces the original function in the namespace is always the innermost layer. The
@functools.wraps(func)annotation must be placed on that function, not on the middledecoratorlayer. - functools.update_wrapper provides advanced control. When you need to copy additional attributes like
__defaults__or__kwdefaults__, or when you need to skip certain default attributes, callfunctools.update_wrapperdirectly with a customassignedtuple.
Metadata preservation is the line between a decorator that works and a decorator that works correctly. The function still executes either way, but everything around it -- debugging, documentation, logging, registration, testing -- depends on the metadata being intact. One line of @functools.wraps(func) ensures that a decorated function retains its identity no matter how many layers of wrapping surround it.