Python Decorators Cheat Sheet
Python decorator patterns including basic function wrappers, functools.wraps, parameterized decorators, class-based decorators, and common built-in decorators.
Basic Decorator
A function that wraps another function's behavior.
def my_decorator(func): def wrapper(*args, **kwargs): print("Before call") result = func(*args, **kwargs) print("After call") return result return wrapper@my_decoratordef greet(name): print(f"Hello, {name}")greet("Alice")# Before call / Hello, Alice / After call
Preserving Metadata with functools.wraps
Keeping the wrapped function's name and docstring intact.
from functools import wrapsdef my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper@my_decoratordef greet(name): """Greets someone.""" return f"Hello, {name}"greet.__name__ # 'greet', not 'wrapper'greet.__doc__ # 'Greets someone.'
Decorators with Arguments
A decorator factory that accepts its own parameters.
from functools import wrapsdef repeat(times): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): result = None for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator@repeat(times=3)def say_hi(): print("Hi!")say_hi() # prints Hi! three times
Class-Based Decorators
Using a class with __call__ as a stateful decorator.
class CountCalls: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"Call #{self.count}") return self.func(*args, **kwargs)@CountCallsdef hello(): print("Hello")hello() # Call #1 / Hellohello() # Call #2 / Hello
Common Built-in Decorators
Decorators provided by Python's standard library.
- @staticmethod- defines a method with no implicit self or cls argument
- @classmethod- defines a method that receives the class as its first argument
- @property- exposes a method as a read-only attribute
- @functools.lru_cache- memoizes a function's return values by its arguments
- @functools.cached_property- computes an instance property once and caches the result
- @dataclasses.dataclass- auto-generates __init__, __repr__, and __eq__ for a class
functools.singledispatch for Generic Functions
Dispatches a function implementation based on the type of its first argument.
from functools import singledispatch@singledispatchdef render(value): raise NotImplementedError(f"No renderer for {type(value)}")@render.registerdef _(value: int): return f"int: {value}"@render.registerdef _(value: list): return f"list of {len(value)} items"@render.register(str)def _(value): return f"str: {value!r}"render(42) # 'int: 42'render([1, 2, 3]) # 'list of 3 items'
Decorating async Functions
The wrapper itself must be async and await the wrapped coroutine.
import timefrom functools import wrapsdef atiming(func): @wraps(func) async def wrapper(*args, **kwargs): start = time.perf_counter() result = await func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} took {elapsed:.4f}s") return result return wrapper@atimingasync def fetch_data(): import asyncio await asyncio.sleep(0.1) return {"ok": True}# await fetch_data() # prints elapsed time, returns {'ok': True}
Retry Decorator with Exponential Backoff
A parametrized decorator that retries a flaky call with increasing delay.
import timefrom functools import wrapsdef retry(exceptions=(Exception,), tries=3, delay=1, backoff=2): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): _tries, _delay = tries, delay while _tries > 1: try: return func(*args, **kwargs) except exceptions as e: print(f"{e!r}, retrying in {_delay}s...") time.sleep(_delay) _tries -= 1 _delay *= backoff return func(*args, **kwargs) # final attempt, let it raise return wrapper return decorator@retry(exceptions=(ConnectionError,), tries=4, delay=0.5)def call_api(): ...
Decorator Usable With or Without Parentheses
Detects whether it was called as @deco or @deco(...) using a single positional callable check.
from functools import wrapsdef trace(func=None, *, label="CALL"): def decorator(f): @wraps(f) def wrapper(*args, **kwargs): print(f"[{label}] {f.__name__}") return f(*args, **kwargs) return wrapper if func is not None: return decorator(func) # used as @trace return decorator # used as @trace(label=...)@tracedef a(): ...@trace(label="DB")def b(): ...
Advanced Decorator Gotchas
Subtle behaviors that trip up decorator authors once past the basics.
- Order of stacked decorators- applied bottom-up but their side effects at call time run top-down; misreading this order is the #1 source of bugs
- Decorators run at import/definition time- the outer decorator body executes once when the module loads, not on every call
- Losing signature introspection- without @wraps, tools like inspect.signature() and help() show the wrapper's (*args, **kwargs), not the original
- Decorating methods vs functions- a decorator applied to a method receives the bound/unbound function; self is just args[0] at wrap time
- Stateful decorators and thread safety- a class-based decorator with mutable state (like a call counter) is shared across all calls; guard it with a lock if used concurrently
- functools.wraps(func)(wrapper)- the imperative form, useful when you can't use the @wraps syntax, e.g. building wrappers dynamically in a loop
Stack decorators bottom-up in your head: `@a` then `@b` above a function means `a(b(func))`, so the decorator closest to the def runs first when wrapping and last when the wrapped call actually executes.