Back to Blog
Python

Using functools.wraps to Preserve Function Metadata

python functools wraps: Learn how functools.wraps preserves function metadata in Python decorators, preventing name and docstring loss.

decoratorsfunction metadatafunctoolspythonwrapper functions
Illustration of a Python function being wrapped with metadata preserved by functools.wraps

When you write a decorator in Python, you typically return a wrapper function. Without any help, that wrapper has its own name, docstring, and signature, which can break introspection tools, debugging, and documentation generation. functools.wraps is a helper that copies the metadata from the original function to the wrapper, so the decorated function looks like the original. This article explains how python functools wraps works, why it matters, and when to use it.

What functools.wraps Does

functools.wraps is a decorator that updates the wrapper function to mirror the metadata of the original function. It copies attributes such as __name__, __doc__, __module__, __qualname__, __annotations__, and __dict__. It also sets __wrapped__ to the original function, which allows tools like inspect.signature to follow the original signature even when the wrapper has a different one.

Without wraps, a decorated function loses its identity. For example, a simple logging decorator that does not use wraps will cause the decorated function to report the wrapper's name and no docstring.

The Problem Without wraps

Consider a simple decorator that logs calls:

def log_calls(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @log_calls def add(a, b): """Return the sum of a and b.""" return a + b

Now inspect add:

print(add.__name__) # 'wrapper' print(add.__doc__) # None

The original name and docstring are lost. This can break tools that rely on introspection, such as debuggers, test runners, and documentation generators. For example, help(add) would show the wrapper's signature and no useful docstring, making the function harder to use.

Using functools.wraps

The fix is to apply functools.wraps to the wrapper function:

import functools def log_calls(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper

Now add.__name__ is 'add' and add.__doc__ is the original docstring. wraps also updates other attributes like __module__, __qualname__, and __annotations__. The decorated function behaves as if it were the original, which is especially important when the decorator is used in a library or framework that relies on function metadata.

How wraps Works Under the Hood

functools.wraps is itself a decorator that calls functools.update_wrapper(wrapper, wrapped). update_wrapper copies the relevant attributes from the wrapped function to the wrapper and sets __wrapped__ to the original. The __wrapped__ attribute is particularly useful for introspection: inspect.signature follows it by default, so the signature of the decorated function matches the original even if the wrapper accepts *args, **kwargs. This transparency is crucial for maintaining accurate documentation and type checking.

Practical Example: A Timing Decorator

A common use case is a decorator that measures execution time:

import functools import time def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.6f} seconds") return result return wrapper @timer def process_data(data): """Process a list of items.""" return [item * 2 for item in data]

Without wraps, the printed name would be 'wrapper' instead of 'process_data'. With wraps, the decorator remains transparent to the rest of the code. This matters when you have multiple decorated functions and need to identify which one is being called from logs or error messages.

When You Should and Shouldn't Use wraps

You should use wraps for almost any decorator that returns a function, especially when the decorator is meant to be reusable or part of a library. It ensures that the decorated function retains its identity, which is the expected behavior in most Python code. However, there are cases where you might intentionally avoid it:

  • If you want the wrapper to have a distinct name for debugging, though this is rare and usually a sign that the decorator should be restructured.
  • If you are creating a decorator that changes the signature or behavior so drastically that preserving the original metadata would be misleading. For instance, a decorator that transforms a function into a class might not want to copy the original __name__.
  • When you are using a class-based decorator, wraps is still applicable but requires careful handling. You can apply functools.wraps to the __call__ method, but you must ensure the instance itself has the desired metadata.

Compatibility and Maintainability Considerations

functools.wraps works with functions and any callable object that has the attributes update_wrapper expects. If the wrapped object is a class or a callable instance, you may need to ensure it has __name__, __doc__, and other attributes. In Python 3, update_wrapper also copies __dict__ by default, so any custom attributes on the original function are preserved. This is helpful when you attach metadata to functions, such as a __author__ or a custom __description__.

The __wrapped__ attribute set by wraps is used by some tools to unwrap the function. For example, inspect.unwrap follows the chain of __wrapped__ references. This can be useful for debugging but also means that code that explicitly checks __name__ will see the original name, which is usually the desired behavior. However, be aware that if you nest multiple decorators, each with wraps, the __wrapped__ chain can become long. This is generally harmless, but it is worth knowing when you rely on deep introspection.

In practice, functools.wraps is a small but essential tool for writing clean, maintainable decorators. It prevents subtle bugs where function identity is lost, and it keeps your codebase consistent with Python's introspection conventions. Whenever you write a decorator that returns a wrapper function, applying functools.wraps should be your default choice.

python functools wraps: Preserve Function Metadata | RYUSLOG DEV