Skip to content

Decorators

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 decoration
say_hello()
# Before
# Hello!
# After

The @decorator syntax is shorthand — it applies the decorator automatically right after the function is defined. These two are exactly equivalent:

@my_decorator
def say_hello():
print("Hello!")
# is identical to:
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)

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_decorator
def 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.

import functools
import 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
@timed
def compute(n: int) -> int:
return sum(range(n))

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).

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__)
What does `@my_decorator` above a function definition do?
Why should you use `@functools.wraps(func)` inside a decorator?
Given `@retry(max_attempts=3)` above `def fetch(): ...`, what is the call order?
If decorators are stacked `@a` then `@b` then `@c` on `def f()`, which is applied first?