Back to Blog
Python

Python wraps Decorator: Preserving Function Metadata

Learn how the python wraps decorator preserves function metadata, improves debugging, and keeps your decorators clean and maintainable.

PythonDecoratorsfunctoolsFunction MetadataMaintainability
A Python code editor showing a decorated function with metadata preserved by functools.wraps, symbolized by a wrapping ribbon around a function name.

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

When you write a Python decorator without wrapping the inner function, the decorated function loses its original name, docstring, and other metadata. This breaks introspection, confuses debugging tools, and makes help() output misleading. The functools.wraps decorator solves this by copying the original function's attributes onto the wrapper. Here is how it works and where it fits into real code.

Why Decorators Lose Function Metadata

A decorator is just a callable that takes a function and returns a replacement. When you apply a decorator, the name in the module namespace points to the returned wrapper, not the original function. If the wrapper does not copy the original attributes, it becomes the only thing visible to callers.

Consider this minimal decorator without wraps:

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

The add function now reports its name as wrapper and has no docstring. Any tool that relies on introspection, such as help(), pydoc, or a debugger, will show the wrapper instead of the real function. This is a common source of confusion, especially when decorators are used in libraries or frameworks.

How functools.wraps Fixes the Problem

The functools.wraps decorator is a thin wrapper around functools.update_wrapper. It copies a set of attributes from the original function to the wrapper, including __name__, __doc__, __module__, __qualname__, __dict__, and __wrapped__. The __wrapped__ attribute is particularly useful because it lets tools like inspect.signature follow the chain back to the original function.

Applying wraps is straightforward:

from functools import wraps def log_call(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @log_call def add(a, b): """Return the sum of a and b.""" return a + b print(add.__name__) # 'add' print(add.__doc__) # 'Return the sum of a and b.'

The decorated function now looks and behaves like the original from the caller's perspective. The wrapper still executes the logging logic, but the metadata is preserved.

Using wraps in a Real Decorator

A common use case is a decorator that validates arguments or caches results. Without wraps, every decorated function becomes anonymous in stack traces and documentation. With wraps, the original identity remains intact.

Here is a practical example that caches the result of a function based on its arguments:

from functools import wraps def memoize(func): cache = {} @wraps(func) def wrapper(*args): if args not in cache: cache[args] = func(*args) return cache[args] return wrapper @memoize def factorial(n): """Return n!.""" if n <= 1: return 1 return n * factorial(n - 1) print(factorial.__name__) # 'factorial' print(factorial.__doc__) # 'Return n!.'

The wraps decorator also works when the decorator itself takes arguments. In that case, you apply wraps inside the inner decorator:

def repeat(times): def decorator(func): @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): """Say hello.""" return f"Hello, {name}"

This pattern keeps the decorator factory clean and ensures the final function retains its metadata.

What wraps Actually Copies

The update_wrapper function, which wraps calls, copies a predefined set of attributes. The exact list is defined in the WRAPPER_ASSIGNMENTS tuple, which by default includes:

  • __module__
  • __name__
  • __qualname__
  • __doc__
  • __annotations__

It also updates the wrapper's __dict__ with the original function's __dict__ entries, and sets __wrapped__ to the original function. The __wrapped__ attribute is what allows inspect.signature to traverse the wrapper and report the original signature, even though the wrapper itself has a generic (*args, **kwargs) signature.

You can customize which attributes are copied by passing the assigned and updated parameters to wraps, but the defaults cover the vast majority of cases.

When wraps Is Not Enough

wraps preserves metadata, but it does not automatically make the wrapper's signature match the original function. For most code, this is acceptable because inspect.signature follows __wrapped__. However, if you are writing a decorator that needs to expose a true signature at runtime—for example, when building a framework that validates arguments based on the decorated function's parameters—you may need to use functools.wraps together with inspect.signature or a library like decorator that reconstructs the signature explicitly.

Another limitation is that wraps only copies attributes that exist on the original function. If the original function has a custom attribute that is not in the default assignment list, it will not be copied unless you extend assigned. For example:

from functools import wraps def add_attr(func): func.custom = 42 return func @add_attr def example(): pass @wraps(example) def wrapper(): pass print(hasattr(wrapper, 'custom')) # False

In such cases, you need to explicitly copy the attribute or use update_wrapper with a custom assigned list.

Performance and Maintainability Considerations

The runtime cost of wraps is negligible. It performs a few attribute assignments once when the decorator is applied, not on every call. The wrapper itself adds a small overhead per invocation because it introduces an extra function call, but that is true for any decorator. wraps does not change that.

The real benefit is maintainability. When a decorated function preserves its name and docstring, debugging becomes easier because tracebacks show the original function name. Tools like help() and IDE autocompletion work as expected. This is especially valuable in large codebases where decorators are used across many modules.

One operational concern is that wraps sets __wrapped__, which some serialization or mocking frameworks might follow. If you are using a library that inspects __wrapped__ to unwrap functions, be aware that it will now see the original function. This is usually desirable, but it can cause unexpected behavior if the original function is not importable or if you rely on the wrapper being the only visible layer.

Preserving Signatures with inspect.signature

Because wraps sets __wrapped__, inspect.signature can automatically resolve the original signature. This means that even though the wrapper accepts *args, **kwargs, tools that use inspect.signature will report the original parameters. For example:

import inspect from functools import wraps def log_call(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @log_call def add(a, b): """Return the sum.""" return a + b print(inspect.signature(add)) # (a, b)

This behavior is built into Python's standard library, so you do not need extra code to make it work. However, if you are writing a decorator that must work with older Python versions that do not follow __wrapped__ in inspect.signature, you may need to set __signature__ manually. In modern Python (3.4+), the __wrapped__ mechanism is supported, so wraps is sufficient for most use cases.

Common Pitfalls and How to Avoid Them

One common mistake is applying wraps to the wrong function. The @wraps(func) decorator must be placed directly above the wrapper definition. If you put it above the outer decorator function, it will not work as intended. Another mistake is forgetting to return the wrapper from the decorator, which results in the original function being replaced by None. Always ensure the decorator returns the wrapped function.

Another subtle issue is that wraps does not copy __signature__ directly. If you have a decorator that modifies the function's parameters, you may need to update __signature__ yourself. For example, a decorator that removes a parameter from the callable should reflect that in the signature. In that case, you can set wrapper.__signature__ to a new inspect.Signature object after applying wraps. This is an advanced scenario, but it is worth knowing when you build decorators that change the call interface.

Finally, remember that wraps only works on functions, not on classes or other callables. If you are writing a decorator that can be applied to both functions and classes, you need to handle the class case separately, often by using functools.update_wrapper with a custom assignment list or by using wraps only when the target is a function.

python wraps decorator: Preserve Function Metadata | RYUSLOG DEV