Batu Lab NotesPractical developer guides

Keep decorator metadata with functools.wraps

By Batu · English technical notes

Also published in our Blogger archive.

Keep decorator metadata with functools.wraps

A decorator commonly returns an inner wrapper function, so metadata can accidentally describe wrapper instead of the function a caller decorated. In the example, announce prints the target name before calling it. Applying @functools.wraps(func) to the inner function copies the usual identifying metadata and sets __wrapped__ to the original function.

After decoration, total_units(2, 6) prints its call notice and returns 12. The assertions show that its public name remains total_units, its docstring remains available, its annotations retain the two integer parameters and integer return type, and __wrapped__ refers to a callable that produces the original result. This helps documentation systems, debuggers, and introspection tools identify the intended function rather than an anonymous wrapper.

wraps does not make the wrapper identical to the wrapped function. The wrapper’s implementation still accepts *args and **kwargs, and its added logging can change timing, output, or exception context. Although inspect.signature normally follows __wrapped__, code that disables unwrapping can see the wrapper signature. Metadata copying also cannot repair a decorator that changes arguments or returns the wrong value. Keep the wrapper small and test its behavioral contract separately.

AI assistance disclosure: This article was prepared with AI assistance.

Example

from functools import wraps


def announce(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


@announce
def total_units(boxes: int, units_per_box: int) -> int:
    """Return the number of packed units."""
    return boxes * units_per_box


result = total_units(2, 6)
assert total_units.__name__ == "total_units"
assert total_units.__doc__ == "Return the number of packed units."
assert total_units.__annotations__ == {
    "boxes": int,
    "units_per_box": int,
    "return": int,
}
assert total_units.__wrapped__(2, 6) == 12
print(result)
print(total_units.__name__)

Expected output

calling total_units
12
total_units

Source: Python functools.wraps documentation.