23.3 Preserving Metadata with functools.wraps
When you create a decorator in Python, you are essentially creating a function that wraps another function. While this is powerful, it introduces a significant problem: the original function’s identity is lost. The wrapper function created by the decorator replaces the original function object. This means crucial metadata—such as the function’s name (__name__), its docstring (__doc__), and its module (__module__)—are overwritten with the wrapper’s metadata. This loss of information breaks introspection tools and can make debugging and logging exceptionally difficult.
Consider a simple decorator that logs function calls:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@simple_decorator
def greet(name):
"""Return a friendly greeting."""
return f"Hello, {name}!"
print(greet.__name__) # Output: 'wrapper'
print(greet.__doc__) # Output: None
Here, greet’s name is now 'wrapper' and its docstring is gone. This is because the greet variable now points to the wrapper function inside the decorator, not the original greet function.
The Role of functools.wraps
The functools.wraps decorator, itself a decorator helper, is the standard solution to this problem. It is designed to be used inside your decorator, applied to the wrapper function. Its sole purpose is to copy the metadata from the original function (func) to the wrapper function, thereby preserving the decorated function’s identity.
import functools
def proper_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@proper_decorator
def greet(name):
"""Return a friendly greeting."""
return f"Hello, {name}!"
print(greet.__name__) # Output: 'greet'
print(greet.__doc__) # Output: 'Return a friendly greeting.'
greet("Alice") # Output: Calling greet\nHello, Alice!
By applying @functools.wraps(func) to the wrapper function, we instruct Python to copy attributes like __name__, __doc__, and __module__ from func to wrapper before the decorator returns it. This makes the decorated function behave much more like the original from an introspection perspective.
What Metadata Does functools.wraps Preserve?
functools.wraps does more than just copy the name and docstring. It updates the wrapper function to look like the wrapped function by copying a suite of attributes. By default, it copies:
__annotations____doc____module____name____qualname__
It also updates the __dict__ of the wrapper to include the __dict__ of the original function, allowing any custom attributes set on the original to be accessible. Furthermore, it sets a special __wrapped__ attribute that points directly to the original, undecorated function. This allows for advanced introspection, such as unwrapping a decorated function if needed.
# Continuing from the previous example
print(greet) # Output: <function greet at 0x...>
print(greet.__wrapped__) # Output: <function greet at 0x...> (a different memory address)
original_greet = greet.__wrapped__
print(original_greet("Bob")) # Output: Hello, Bob! (no logging)
Common Pitfalls and Best Practices
A critical pitfall is forgetting to use functools.wraps altogether, which leads to the confusing behavior demonstrated initially. Always use it; the cost is negligible, and the benefits for debugging and tooling are immense.
Another subtle issue arises with stacked decorators (multiple decorators on one function). The order of decoration matters. functools.wraps ensures that each layer of decoration correctly preserves the metadata of the function it immediately wraps. If you stack decorators, the metadata will ultimately point to the correct original function, provided each decorator in the chain uses wraps.
@decorator_a
@decorator_b
def my_function():
pass
# The process is: decorator_b wraps my_function, then decorator_a wraps the wrapper from decorator_b.
# Thanks to `wraps`, the final function's metadata will still be for `my_function`.
It’s also considered a best practice to use functools.wraps for consistency and future-proofing. Even if you don’t currently use the docstring or other metadata, a future maintainer (or yourself) might, or a library might rely on it. Using wraps is a hallmark of a well-crafted, professional-grade decorator.
In summary, functools.wraps is a non-negotiable tool for writing correct decorators. It maintains the decorated function’s identity, ensuring that introspection, debugging, and documentation tools continue to work as expected, making your code more robust and professional.