Skip to content
Python Basics: Preserving Function Metadata in Decorators

Python Basics: Preserving Function Metadata in Decorators

October 19, 2026

A decorator can add behavior around a function call. A small wrapper is easy to write, but preserving the original function name and documentation makes debugging and inspection much clearer.

These three standalone examples were tested on Linux with Python 3.12.14. They require no third-party packages. Assertions check the results; run without Python’s -O option.

Prepare a scratch directory

You need Bash, Python 3.12 or newer on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep using the same terminal and save each named source file as UTF-8.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

1. Preserve the name and docstring

Save as trace_function.py:

from functools import wraps


def trace(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        print("Calling:", function.__name__)
        return function(*args, **kwargs)
    return wrapper


@trace
def label(name, *, prefix="Hi"):
    """Build a greeting label."""
    return f"{prefix}, {name}"


result = label("Ada", prefix="Hello")
assert result == "Hello, Ada"
assert label.__name__ == "label"
assert label.__doc__ == "Build a greeting label."
print(result)
print(label.__name__)
print(label.__doc__)

Run it:

python3 trace_function.py

Output:

Calling: label
Hello, Ada
label
Build a greeting label.

@trace replaces label with the wrapper returned by trace. @wraps(function) copies useful metadata to that wrapper. Forward both positional and keyword arguments, and return the wrapped function’s result.

2. Configure a decorator

Save as prefix_result.py:

from functools import wraps


def add_prefix(prefix):
    def decorate(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            return prefix + function(*args, **kwargs)
        return wrapper
    return decorate


@add_prefix("Status: ")
def status(name):
    return name.upper()


assert status("ready") == "Status: READY"
assert status.__name__ == "status"
print(status("ready"))
print(status.__name__)

Run it:

python3 prefix_result.py

Output:

Status: READY
status

Calling add_prefix creates a decorator that remembers its prefix. The inner wrapper runs later, each time status is called. This example expects the decorated function to return a string.

3. Inspect the underlying function

Save as inspect_wrapper.py:

from functools import wraps
from inspect import signature


def announce(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        print("Using wrapper")
        return function(*args, **kwargs)
    return wrapper


@announce
def multiply(value, *, factor=2):
    return value * factor


assert str(signature(multiply)) == "(value, *, factor=2)"
assert multiply.__wrapped__(3, factor=4) == 12
print(signature(multiply))
print(multiply(3, factor=4))
print(multiply.__wrapped__(3, factor=4))

Run it:

python3 inspect_wrapper.py

Output:

(value, *, factor=2)
Using wrapper
12
12

wraps adds __wrapped__, which points to the original function. inspect.signature follows that reference by default. Calling it directly bypasses the wrapper, so decorator-based checks are not a security boundary.

When to use this

Use wrappers for small shared behaviors. wraps preserves metadata; it does not implement argument validation or turn a synchronous wrapper into an asynchronous one. Keep the return value and exception behavior deliberate.

References

Clean up

In the same terminal, remove only these example files, then return to your original directory and remove the empty scratch directory:

rm -- trace_function.py prefix_result.py inspect_wrapper.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on