36.5 functools.wraps: Preserving Decorator Metadata
When decorating a function, a common pitfall is that the original function’s metadata—its name (__name__), docstring (__doc__), and other attributes—are replaced by those of the wrapper function inside the decorator. This loss of information breaks introspection tools and can make debugging and logging significantly more difficult. The functools.wraps decorator is the standard solution to this problem, acting as a decorator itself to apply to the wrapper function within your decorator. It copies the critical metadata from the original function to the wrapper, preserving the decorated function’s identity.
The Problem: Lost Metadata in Decorators
To understand the value of functools.wraps, one must first understand the problem it solves. A basic decorator that times a function call might look like this:
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Executed {func.__name__} in {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer_decorator
def calculate_sum(n):
"""Calculates the sum of numbers from 1 to n."""
return sum(range(1, n+1))
print(calculate_sum.__name__) # Output: 'wrapper'
print(calculate_sum.__doc__) # Output: None
Here, the decorated calculate_sum function has lost its original name and docstring. Its __name__ is now ‘wrapper’, and its __doc__ is None. This is because the decorator returns the wrapper function, which has its own metadata. This is problematic for any tool that relies on introspection, such as help systems, debuggers, or other decorators.
The Solution: Using functools.wraps
The functools.wraps function is a decorator factory designed to be applied to the wrapper function inside your decorator. It takes the original function (func) as an argument and updates the wrapper function’s metadata to match it.
import functools
import time
def timer_decorator(func):
@functools.wraps(func) # Apply wraps to the wrapper, passing the original func
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"Executed {func.__name__} in {end_time - start_time:.4f} seconds")
return result
return wrapper
@timer_decorator
def calculate_sum(n):
"""Calculates the sum of numbers from 1 to n."""
return sum(range(1, n+1))
print(calculate_sum.__name__) # Output: 'calculate_sum'
print(calculate_sum.__doc__) # Output: 'Calculates the sum of numbers from 1 to n.'
help(calculate_sum) # Correctly shows the function name and docstring
By adding @functools.wraps(func), the wrapper function’s __name__, __doc__, __module__, and __annotations__ are copied from the original func. The identity of calculate_sum is now preserved.
What Metadata Does wraps Preserve?
functools.wraps does more than just copy the name and docstring. It updates the wrapper with a specific set of attributes to make it resemble the original function as closely as possible. You can see exactly what it does by examining its __wrapped__ attribute, which it also sets.
# Continuing from the previous example
print(calculate_sum.__module__) # Output: '__main__'
print(hasattr(calculate_sum, '__wrapped__')) # Output: True
# The __wrapped__ attribute provides direct access to the original function.
original_func = calculate_sum.__wrapped__
print(original_func(5)) # Output: 15 (executes without timing)
The __wrapped__ attribute is particularly important as it allows advanced introspection tools and other decorators to “peel back” the layers of decoration to access the original function. This is a key best practice for writing well-behaved decorators.
Best Practices and Common Pitfalls
Always Use wraps: It should be considered mandatory for any decorator you write that returns a wrapper function. The cost is minimal (one extra line), and the benefit for usability and debugging is enormous.
Order of Decorator Application Matters: When multiple decorators are stacked,
wrapspreserves the metadata from the most recently applied decorator’s perspective. The__wrapped__attribute allows chaining.@decorator2 @decorator1 @timer_decorator # This is applied first, its wrapper becomes the "original" for decorator1 def my_function(): passCustomizing wraps: The
functools.wrapsdecorator accepts an optionalassignedparameter to specify which attributes to copy (defaults tofunctools.WRAPPER_ASSIGNMENTS) and an optionalupdatedparameter to specify which attributes to update in the wrapper (defaults tofunctools.WRAPPER_UPDATES, which includes the__dict__attribute). You rarely need to change these, but the capability exists for edge cases.Pitfall: Forgetting to Use it: The most common mistake is simply forgetting to add
@functools.wraps(func). This leads to the subtle and frustrating bugs described earlier, where functions suddenly have the wrong name in tracebacks or logs.
In summary, functools.wraps is an essential tool for writing professional, robust decorators. It ensures that the decorated function maintains its introspective integrity, which is crucial for debugging, logging, and the proper functioning of other tools within the Python ecosystem. Its consistent use is a hallmark of high-quality, maintainable code.