Decorators
Functions that transform functions
Section titled “Functions that transform functions”A decorator is a function that takes a function as its argument and returns a new function.
Because functions are first-class objects in Python, a decorator is just an ordinary function — there is no special syntax involved until you add @.
def my_decorator(func): def wrapper(*args, **kwargs): print("Before") result = func(*args, **kwargs) print("After") return result return wrapper
def say_hello(): print("Hello!")
say_hello = my_decorator(say_hello) # manual decorationsay_hello()# Before# Hello!# AfterThe @ syntax
Section titled “The @ syntax”The @decorator syntax is shorthand — it applies the decorator automatically right after the function is defined.
These two are exactly equivalent:
@my_decoratordef say_hello(): print("Hello!")
# is identical to:def say_hello(): print("Hello!")say_hello = my_decorator(say_hello)Preserving metadata with functools.wraps
Section titled “Preserving metadata with functools.wraps”Without functools.wraps, the wrapper function replaces the original’s __name__, __doc__, and other attributes.
This breaks introspection, logging, and documentation tools.
import functools
def my_decorator(func): @functools.wraps(func) # copies __name__, __doc__, __annotations__, etc. def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper
@my_decoratordef add(a: int, b: int) -> int: """Return the sum of a and b.""" return a + b
print(add.__name__) # add (not 'wrapper')print(add.__doc__) # Return the sum of a and b.Always apply @functools.wraps(func) to the inner wrapper function in every decorator you write.
A practical logging decorator
Section titled “A practical logging decorator”import functoolsimport time
def timed(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} finished in {elapsed:.6f}s") return result return wrapper
@timeddef compute(n: int) -> int: return sum(range(n))Decorators that take arguments
Section titled “Decorators that take arguments”To pass arguments to a decorator, add one more level of nesting: a factory function that accepts the arguments and returns the actual decorator.
import functools
def retry(max_attempts: int = 3): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as exc: if attempt == max_attempts: raise print(f"Attempt {attempt} failed: {exc}. Retrying...") return wrapper return decorator
@retry(max_attempts=3)def fetch(url: str) -> str: # would call an HTTP client here return f"Response from {url}"@retry(max_attempts=3) first calls retry(max_attempts=3), which returns decorator.
Python then applies decorator to fetch — so the full call chain is fetch = retry(3)(fetch).
Full runnable demo
Section titled “Full runnable demo”import functools
def log_calls(level: str = "INFO"): """Decorator factory: log calls at the given level.""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): arg_str = ", ".join( [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()] ) print(f"[{level}] {func.__name__}({arg_str}) called") result = func(*args, **kwargs) print(f"[{level}] {func.__name__} returned {result!r}") return result return wrapper return decorator
@log_calls(level="DEBUG")def add(a: int, b: int) -> int: """Return the sum of a and b.""" return a + b
@log_calls()def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!"
print("--- add ---")add(3, 4)
print("--- greet ---")greet("Alice")greet("Bob", greeting="Hi")
print("--- metadata preserved ---")print(add.__name__)print(add.__doc__)Loading Python runtime (first run only)…