Create and stack function decorators in Python
Wrap a function while preserving its metadata and return value, add decorator arguments, and distinguish wrapping order from call order.
A function decorator receives a function and returns the object that will replace it. A common decorator returns a wrapper that runs extra code around the original call. Apply it with @decorator above the function definition.
Create a wrapper
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before the call")
result = func(*args, **kwargs)
print("After the call")
return result
return wrapper
@my_decorator
def greet(name):
return f"Hello, {name}"
print(greet("Sam"))
*args and **kwargs forward arguments, and returning result preserves the normal return value. functools.wraps() copies useful metadata such as the name and docstring and adds a __wrapped__ reference. In this example, the “After” message runs only when the function returns normally; use finally for cleanup that must also run on failure.
Accept decorator arguments
An extra function layer captures the options before constructing the decorator:
import functools
import time
def timing(unit="s"):
factors = {"s": 1, "ms": 1000}
if unit not in factors:
raise ValueError("unit must be 's' or 'ms'")
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = (time.perf_counter() - start) * factors[unit]
print(f"{func.__name__}: {elapsed:.4f} {unit}")
return wrapper
return decorator
@timing(unit="ms")
def slow_func():
time.sleep(0.01)
slow_func()
perf_counter() returns seconds, so displaying milliseconds requires multiplication by 1,000, not merely changing the label. The timer reports on normal or exceptional exits and leaves the original exception to propagate.
Understand stacking order
@decorator_a
@decorator_b
def operation():
pass
Given those decorators, the definition is equivalent to operation = decorator_a(decorator_b(operation)). The lower decorator wraps first. For wrappers that each run code before and after their inner call, invocation proceeds A-before, B-before, original function, B-after, A-after. Exceptions, early returns, and wrappers that skip the call can change that sequence.
FAQ
Does this work with instance methods? A normal wrapper that forwards all arguments forwards self too. Descriptor decorators such as classmethod can make stacking order significant, so test the intended combination.
Can a decorator receive a class? Yes. Class decorators receive a class object; dataclass is a familiar example. Decorators are not restricted to functions that return plain functions.
Can I bypass the wrapper? function.__wrapped__ accesses the next wrapped layer when it was provided. It does not necessarily remove an entire stack; inspect.unwrap() follows the chain. Bypassing wrappers can also bypass intended checks or instrumentation.
What about async functions? A synchronous wrapper only times creation of a coroutine object. Use an async wrapper that awaits the function inside its try block when you need to observe its execution.
References
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan