Python Basics: Preserving Function Metadata in Decorators
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 11. 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.pyOutput:
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.pyOutput:
Status: READY
statusCalling 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.pyOutput:
(value, *, factor=2)
Using wrapper
12
12wraps 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_dirAcknowledgements
This post was created with assistance from AI.