Preserve Python Function Metadata with functools.wraps

Learn why decorators erase function metadata and how functools.wraps preserves the original name, docstring, and attributes of wrapped functions.

7 min read

Every Python decorator replaces one function with another. The original function, with its name, docstring, and carefully written documentation, gets swapped for a wrapper that the decorator author defined. If you do nothing to preserve the original metadata, the replacement function reports a generic name and an empty docstring to every tool that inspects it. When you write decorators, you need functools.wraps to preserve function metadata. It is a tiny decorator from the standard library that copies the original function's identity onto the wrapper, and it belongs in every decorator you write.

The problem is easiest to see with a concrete example. Take a simple decorator that adds no behaviour at all, just wraps and forwards. Apply it to a function that has a meaningful name and a docstring, then inspect the decorated function. The results are surprising the first time you see them:

pythonpython
def null_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
 
@null_decorator
def calculate_tax(amount, rate):
    """Compute sales tax for a given amount and rate."""
    return amount * rate
 
print(calculate_tax.__name__)
print(calculate_tax.__doc__)

The output reveals the problem immediately. The name is reported as "wrapper" instead of "calculate_tax", and the docstring is None instead of the original documentation string. The function still works when called, but its identity has been replaced. Any logging framework that records function names, any test runner that reports test names, any documentation generator that extracts docstrings, and any debugger that shows the current callable all see "wrapper" and a missing docstring. In a codebase with dozens of decorated functions, every single one reports the same generic name, making stack traces and log output nearly useless for identifying which function actually ran.

This happens because the at-syntax replaces the function name binding with whatever the decorator returns. The decorator returns the wrapper, and the wrapper is a distinct function object with its own name. The original function's metadata (its name, docstring, module, qualified name, and annotations) lives on the original function object, which is now referenced only by the closure variable inside the wrapper. Python has no built-in mechanism to forward metadata automatically because decorators can return anything, not just a thin wrapper, and the language cannot know which metadata the decorator author intends to preserve.

How functools.wraps solves the problem

The functools module in the standard library provides wraps, a decorator designed specifically for this situation. It is used inside your decorator, applied to the wrapper function, and it copies the original function's metadata onto the wrapper at definition time. The syntax is always the same: you decorate the wrapper with wraps, passing the original function as the argument. Here is the corrected version of the null decorator:

pythonpython
from functools import wraps
 
def null_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

With this single line added, the decorated function now reports its correct name and preserves its docstring. The change is invisible to callers, but every tool that inspects the function sees the original metadata. The wraps decorator copies the function's name, docstring, module, qualified name, annotations, and type parameters from the original to the wrapper. It also sets a special attribute on the wrapper that points back to the original, so you can always recover the undecorated function if needed.

The list of attributes that wraps copies by default is defined by a module-level constant. It includes the attributes that most tools use for identification and documentation. You can customize which attributes are copied by passing the assigned and updated arguments to wraps, though the defaults cover the common case and customizing them is rarely necessary outside of framework-level decorators that need precise control over the wrapper's public surface.

What happens under the hood

The wraps function is not doing anything magical. It calls a lower-level function, update_wrapper, which performs the actual attribute copying. Understanding what update_wrapper does helps you debug situations where the metadata still does not look right after applying wraps. The function takes the wrapper, the wrapped function, and two optional tuples specifying which attributes to copy and which to update. It reads the specified attributes from the original and sets them on the wrapper using direct assignment.

The most important attribute that update_wrapper sets is the special double-underscore wrapped attribute. This attribute stores a reference to the original function, and it is the standard way to recover the original callable from a decorated one. When you are debugging and want to call the undecorated version of a function, or when you want to re-wrap a function with a different decorator, you access it through this attribute. The functools module itself follows this convention: the lru_cache decorator sets it on its cache wrapper so you can always reach the original function, and tools like the standard library's inspect module rely on it to unwrap decorated functions when generating signature information.

The copying is shallow. If the original function has attributes that are mutable objects, the wrapper gets a reference to the same object, not a copy. This is almost always the desired behaviour because function attributes are typically strings (like docstrings) or immutable metadata. If your original function carries mutable attributes that should be independent between the original and the wrapper, you need to handle that copying manually after wraps does its work.

Applying wraps to real decorators

Every decorator you write should include wraps. The timing decorator from creating your first Python decorator benefits from it: without wraps, every timed function reports its name as "wrapper" in the timing output, which defeats the purpose of including the function name in the timing message. Here is the corrected timing decorator:

pythonpython
import time
from functools import wraps
 
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} ran in {elapsed:.4f}s")
        return result
    return wrapper

Notice that the timing message still uses the original function's name through the closure variable. This works because the closure captures the original function before wraps copies its metadata. The wraps decorator only affects the wrapper's own metadata, not the closure variables that the decorator body defines. The timing message correctly reports the decorated function's name because it reads the closure variable, and tools that inspect the decorated function also see the correct name because wraps copied it onto the wrapper.

If you have been following the earlier articles in this section, you know that decorators can accept parameters through a factory function. Applying wraps inside a parametrized decorator requires placing it on the correct wrapper, which is the innermost wrapper inside the decorator function, not the factory. The factory receives configuration arguments, the decorator receives the function, and the wrapper runs at call time. The wraps decorator goes on the wrapper, receiving the function that the decorator received. This three-layer structure can make wraps easy to misplace, so here is the correct placement inside the retry decorator from the article on Python decorators with parameters:

pythonpython
from functools import wraps
import time
 
def retry(max_attempts=3, delay=1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

The wraps call is on the wrapper, receiving func from the decorator's parameter. This is the same pattern as a non-parametrized decorator, just nested one level deeper. The factory function does not participate in metadata preservation because it is not the function that replaces the original.

Why this matters beyond debugging

Preserving function metadata is not just about having pretty tracebacks. Frameworks and libraries inspect function metadata to make decisions. A web framework might use the function name as the default endpoint name. A task queue might use the function name to identify tasks in the admin interface. A serialization library might use the qualified name to reconstruct objects. When the metadata is wrong, these systems behave incorrectly in ways that are difficult to attribute to a missing wraps call.

Documentation generators like Sphinx and pydoc rely on docstrings and function signatures to produce API documentation. If every decorated function in your library has lost its docstring, your API documentation will be empty or misleading. Users of your code will not understand what your functions do, and the documentation gap will not point back to the decorator as the cause.

Testing frameworks also suffer. When a test fails, the test runner reports the test function's name. If the test is decorated and the name is "wrapper", the failure message is unhelpful. When multiple decorated tests fail, all failure messages report the same name, making it impossible to tell which test failed without reading the full traceback. The problem compounds in parametrized test runs where dozens of test variants share decorators, and every single variant reports the same "wrapper" name.

Once you understand functools.wraps, the next natural question is how to build decorators using classes instead of functions. The article on class-based decorators in Python shows how classes with a call method can serve as decorators with more readable state management than nested function closures.

Rune AI

Rune AI

Key Insights

  • functools.wraps copies the original function's name, docstring, and other metadata onto the decorator wrapper.
  • Without wraps, the decorated function reports the wrapper's name ('wrapper') and loses its docstring.
  • wraps also sets wrapped on the wrapper, allowing access to the original function for introspection or bypassing the decorator.
  • The decorator is applied inside the decorator, on the line directly above the wrapper definition.
  • Every decorator should use functools.wraps as a standard practice unless you intentionally want to replace the original metadata.
RunePowered by Rune AI

Frequently Asked Questions

What does functools.wraps do in Python?

The functools.wraps decorator copies the metadata from a wrapped function (the original) to a wrapper function (the replacement). It copies attributes like __name__, __doc__, __module__, __qualname__, __annotations__, and __type_params__ from the original to the wrapper, and also sets a __wrapped__ attribute on the wrapper pointing back to the original. This preserves the function's identity for debugging, introspection, and documentation tools.

What happens if I do not use functools.wraps in my decorator?

Without functools.wraps, the decorated function takes on the wrapper's metadata. The __name__ becomes 'wrapper' instead of the original function name, the docstring is lost, and introspection tools like help() and pydoc show the wrapper's details instead of the original function's. The function still works correctly at runtime, but debugging, logging, and any tooling that inspects function metadata will produce confusing results.

Conclusion

functools.wraps is a one-line addition to any decorator that preserves the decorated function's identity. It costs nothing at runtime beyond a few attribute copies at definition time, and it prevents a category of debugging headaches that are difficult to trace when they occur. Every decorator you write should use it unless you have a specific reason to replace the original metadata with your own.