Back to Blog
Python

Python Decorator Preserve Metadata with functools.wraps

python decorator preserve metadata: Learn how to preserve function metadata like __name__ and __doc__ when writing Python decorators using functools.wraps, with practi...

decoratorsfunctools.wrapsmetadatafunction introspectionPython programming
Illustration of a Python function being wrapped by a decorator while preserving its name and docstring metadata.

python decorator preserve metadata requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you write a decorator in Python, you replace the original function with a wrapper. Without extra care, the wrapper loses the original function's metadata: its name, docstring, annotations, and module. This breaks introspection tools, logging, documentation generators, and debugging. The standard solution is to use functools.wraps to copy that metadata onto the wrapper. This article explains why the loss happens, how functools.wraps solves it, and what to consider when applying it in real code.

What Happens Without Preserving Metadata

Consider a simple decorator that logs the execution time of a function:

import time def timed(func): def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper

Apply it to a function and inspect the result:

@timed def process_data(): """Processes the data pipeline.""" return [i for i in range(1000)] print(process_data.__name__) # 'wrapper' print(process_data.__doc__) # None

The decorated function is now wrapper, not process_data. The docstring is gone. If you use help(process_data) or a debugger, you see the wrapper's signature and no documentation. This is the core problem: the decorator hides the original function's identity.

The Role of functools.wraps

Python's standard library provides functools.wraps to copy the original function's metadata onto the wrapper. It is a decorator that applies functools.update_wrapper to the wrapper function. The typical pattern is:

import functools import time def timed(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:.4f}s") return result return wrapper

Now the decorated function retains its name and docstring:

@timed def process_data(): """Processes the data pipeline.""" return [i for i in range(1000)] print(process_data.__name__) # 'process_data' print(process_data.__doc__) # 'Processes the data pipeline.'

functools.wraps copies the __module__, __name__, __qualname__, __doc__, and __annotations__ attributes from the original function to the wrapper. It also updates the wrapper's __dict__ with the original's, so custom attributes set on the original function are preserved.

What Metadata Is Actually Preserved

functools.update_wrapper copies a specific set of attributes. The default WRAPPER_ASSIGNMENTS tuple includes:

  • __module__
  • __name__
  • __qualname__
  • __annotations__
  • __doc__

It also updates the wrapper's __dict__ with the original function's __dict__ (via WRAPPER_UPDATES). This means any custom attributes you set on the original function are also copied.

AttributeCopied by defaultPurpose
__name__YesFunction name for debugging/logging
__doc__YesDocstring for help() and docs
__annotations__YesType hints for introspection
__module__YesModule where the function is defined
__qualname__YesQualified name for nested functions
__dict__Yes (merged)Custom attributes

Note that the function signature (__signature__) is not copied by default. If you need to preserve the exact call signature for inspect.signature(), you must set it manually or use a library like decorator that handles this.

Using wraps with Custom Decorators That Accept Arguments

When a decorator itself takes arguments, you need an extra layer. The pattern is to have a factory function that returns a decorator. functools.wraps still works in the innermost wrapper:

def repeat(times): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def greet(name): """Greets a person.""" return f"Hello, {name}" print(greet.__name__) # 'greet' print(greet.__doc__) # 'Greets a person.'

The wrapper still replaces the original, but its metadata now matches the original. The behavior is correct, and the function appears as the original to external tools.

Preserving Metadata for Class-Based Decorators

Decorators can also be implemented as classes using __call__. In that case, you need to manually call functools.update_wrapper in the __init__ or __call__ method. A common pattern:

class Retry: def __init__(self, func): self.func = func functools.update_wrapper(self, func) def __call__(self, *args, **kwargs): try: return self.func(*args, **kwargs) except Exception: # retry logic return self.func(*args, **kwargs) @Retry def fetch_data(): """Fetches data from an API.""" return {"ok": True} print(fetch_data.__name__) # 'fetch_data' print(fetch_data.__doc__) # 'Fetches data from an API.'

Here, update_wrapper copies the metadata from func to the instance self. Since the instance is callable, it acts as the decorated function. This approach works but requires explicit handling; forgetting update_wrapper leads to the same metadata loss as before.

Edge Cases and Limitations

functools.wraps does not preserve the function signature. If you use inspect.signature() on a decorated function, you get the wrapper's signature (*args, **kwargs), not the original's. This can break frameworks that rely on signature introspection, such as FastAPI or type-checking tools. To preserve the signature, you can manually set __wrapped__ and use inspect.signature with follow_wrapped=True (the default), but the wrapper's own signature remains generic.

Another limitation is that functools.wraps copies the __dict__ by updating, not replacing. If the wrapper already has attributes, they are kept unless the original has the same key. This is usually fine but can lead to unexpected behavior if you set attributes on the wrapper before calling wraps.

Performance overhead is minimal: update_wrapper runs once at decoration time, not on every call. The wrapper itself adds one extra function call, but that is negligible for most applications. The real cost is the loss of introspectability, which can cause runtime errors in tools that rely on __name__ or __doc__.

Maintainability and Production Considerations

In production, preserving metadata matters for observability. Logging systems often use func.__name__ to identify the source of a log entry. If a decorator strips that name, logs become ambiguous. Error tracebacks show the wrapper instead of the original function, making debugging harder. Documentation tools like Sphinx use docstrings to generate API docs; without wraps, the docs would show the wrapper's docstring (or none).

When you write a decorator that will be reused across a codebase, always apply functools.wraps to the inner wrapper. It costs nothing and prevents subtle bugs. For class-based decorators, call update_wrapper in __init__. If you need signature preservation, consider using the decorator library, which automatically preserves the signature by generating a wrapper with the same parameters. But for most cases, functools.wraps is sufficient and keeps the code standard-library-only.

A final practical detail: functools.wraps also sets the __wrapped__ attribute on the wrapper to point to the original function. This allows inspect.unwrap() to retrieve the original function, which is useful for deep introspection or for bypassing decorators in tests. You can rely on this attribute to access the underlying implementation when needed.

When you build a decorator that preserves metadata, you make your codebase more maintainable and less surprising for other developers who consume your functions. The few extra characters required by @functools.wraps are a small price for the clarity they provide.

python decorator preserve metadata: Practical Usage and Code | RYUSLOG DEV