Python Decorators Cheat Sheet
Quick-reference for every decorator pattern you actually use. Each section includes copy-ready snippets with inline output comments.
Decorator Basics
@decorator above def f() is just f = decorator(f). A decorator receives a function and returns a (usually wrapped) function.
@my_decorator
def greet():
print('Hello!')
# Exactly equivalent to:
# greet = my_decorator(greet)from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
result = func(*args, **kwargs)
print(f'{func.__name__} returned {result}')
return result
return wrapper@my_decorator
def add(a, b):
return a + b
add(2, 3)
# Calling add
# add returned 5
# => 5@functools.wraps: Preserve Metadata
Without @wraps, the wrapper replaces the original function's __name__, __doc__, and __signature__.
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def greet():
"""Say hello."""
pass
greet.__name__ # => 'wrapper' (wrong!)
greet.__doc__ # => None (lost!)from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def greet():
"""Say hello."""
pass
greet.__name__ # => 'greet' (correct!)
greet.__doc__ # => 'Say hello.' (preserved!)Decorator with Arguments (Factory)
Three layers: factory(args) returns decorator(func) returns wrapper(*args, **kwargs).
from functools import wraps
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print('Hello!')
say_hello() # prints 'Hello!' three timesimport time
from functools import wraps
def rate_limit(max_per_second):
min_interval = 1.0 / max_per_second
def decorator(func):
last_call = [0.0]
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.monotonic() - last_call[0]
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_call[0] = time.monotonic()
return func(*args, **kwargs)
return wrapper
return decoratorStacking Decorators
Bottom decorator wraps first. @g above @f means func = g(f(original)). When called, the outer wrapper runs first.
@auth_required # 2. wraps the result of log_calls(fn)
@log_calls # 1. wraps the original function first
def delete_user(user_id):
...
# Equivalent to:
# delete_user = auth_required(log_calls(delete_user))from functools import wraps
def outer(func):
@wraps(func)
def wrapper(*a, **kw):
print('outer before')
result = func(*a, **kw)
print('outer after')
return result
return wrapper
def inner(func):
@wraps(func)
def wrapper(*a, **kw):
print('inner before')
result = func(*a, **kw)
print('inner after')
return result
return wrapper
@outer
@inner
def hello():
print('hello')
hello()
# outer before
# inner before
# hello
# inner after
# outer afterBuilt-in Decorators: property, classmethod, staticmethod
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(5)
c.area # => 78.53975 (no parentheses needed)class Circle:
def __init__(self, radius):
self.radius = radius # uses the setter
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError('Radius must be positive')
self._radius = valueclass Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, s):
y, m, d = map(int, s.split('-'))
return cls(y, m, d)
d = Date.from_string('2026-02-22')
d.year # => 2026class MathUtils:
@staticmethod
def is_even(n):
return n % 2 == 0
MathUtils.is_even(4) # => True@staticmethod receives no implicit first argument. It is just a namespaced function.
Caching Decorators: @lru_cache, @cache
Memoize expensive function calls. Arguments must be hashable.
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(100) # => 354224848179261915075
fib.cache_info()
# => CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)from functools import cache
@cache
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
factorial(10) # => 3628800@cache is shorthand for @lru_cache(maxsize=None). Memory grows without bound.
fib.cache_clear() # Resets all cached results
fib.cache_info() # => CacheInfo(hits=0, misses=0, ...)Class Decorators
Decorators can also wrap classes. The decorator receives the class and returns a (possibly modified) class.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
repr(p) # => "Point(x=1.0, y=2.0)"
p == Point(1, 2) # => True (__eq__ auto-generated)registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class MyPlugin:
pass
registry # => {'MyPlugin': <class 'MyPlugin'>}from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Config:
host: str
port: int = 8080
c = Config('localhost')
# c.host = 'x' # FrozenInstanceError (immutable)Practical Patterns: timer, retry, validate
from functools import wraps
from time import perf_counter
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = perf_counter() - start
print(f'{func.__name__} took {elapsed:.4f}s')
return wrapperfrom functools import wraps
import time
def retry(max_attempts=3, backoff=2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception:
if attempt == max_attempts - 1:
raise
time.sleep(backoff ** attempt)
return wrapper
return decorator
@retry(max_attempts=3, backoff=2)
def fetch_data(url):
...from functools import wraps
def validate_types(**expected):
def decorator(func):
@wraps(func)
def wrapper(**kwargs):
for name, type_ in expected.items():
if name in kwargs and not isinstance(kwargs[name], type_):
raise TypeError(f'{name} must be {type_.__name__}')
return func(**kwargs)
return wrapper
return decoratorDecorating Async Functions
Async function decorators must use async def wrapper and await the original function.
from functools import wraps
from time import perf_counter
def async_timer(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = perf_counter()
try:
return await func(*args, **kwargs)
finally:
print(f'{func.__name__} took {perf_counter() - start:.4f}s')
return wrapper# BROKEN: sync wrapper returns coroutine, never awaits it
def broken(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) # returns coroutine object!
return wrapperA sync wrapper on an async function does not execute the function. Always use async def + await.
Can you write this from memory?
Write a function `get_item()` (no params, body is `pass`) with the `@login_required` decorator applied