A decorator replaces or wraps a callable when its definition is evaluated.
Python decorators: preserve metadata and expose wrapper behavior
Operation contract
The receipt decorator rejects non-positive identifiers before the wrapped lookup runs. functools.wraps preserves the original function name so diagnostics still identify lookup_receipt. The wrapper returns the result rather than discarding it or converting every failure into a success-shaped string.
Failure and ownership boundary
The wrapper changes the call boundary. Stacking decorators changes which checks or side effects happen first, and a synchronous wrapper cannot correctly await an asynchronous function merely by returning its coroutine. Python closures: capture loop values at the intended time and Python asyncio TaskGroup: cancel sibling work and retain failure evidence cover those separate contracts.
Working program
from functools import wraps
def positive_identifier(operation):
@wraps(operation)
def checked(identifier):
if type(identifier) is not int or identifier <= 0:
raise ValueError("positive integer identifier required")
return operation(identifier)
return checked
@positive_identifier
def lookup_receipt(identifier):
return f"receipt:{identifier}"
print(lookup_receipt(41))
print(lookup_receipt.__name__)
try:
lookup_receipt(0)
except ValueError:
print("identifier rejected")Output
receipt:41
lookup_receipt
identifier rejectedCosts and limits
The wrapper adds one validation call and retains a reference to the wrapped callable. Expensive logging, retry or caching wrappers add their own costs; decoration does not reduce underlying work.
Common Mistakes
- Return the wrapped result and preserve metadata deliberately.
- Do not wrap async work with an unexamined synchronous policy.
Connected lessons
Python closures: capture loop values at the intended time, Python functions: define units, validate input and return a result, Python asyncio TaskGroup: cancel sibling work and retain failure evidence.
