Cheatsheet for decorators and context managers.

Basic decorator

from functools import wraps

def log_calls(fn):
    @wraps(fn)
    def inner(*args, **kwargs):
        print(f"calling {fn.__name__}")
        return fn(*args, **kwargs)
    return inner

@log_calls
def add(a, b): return a + b

@wraps(fn) preserves __name__, __doc__, etc.

Decorator with args

def retry(times: int):
    def decorator(fn):
        @wraps(fn)
        def inner(*args, **kwargs):
            for i in range(times):
                try:
                    return fn(*args, **kwargs)
                except Exception:
                    if i == times - 1: raise
        return inner
    return decorator

@retry(times=3)
def flaky(): ...

Three nested functions. Use ParamSpec for typing.

Async decorator

def log_async(fn):
    @wraps(fn)
    async def inner(*args, **kwargs):
        print(f"async {fn.__name__}")
        return await fn(*args, **kwargs)
    return inner

@log_async
async def fetch(): ...

Note: async def inner and await fn(...).

Typed decorator (ParamSpec)

from typing import ParamSpec, Callable, TypeVar
from functools import wraps

P = ParamSpec("P")
R = TypeVar("R")

def log_calls(fn: Callable[P, R]) -> Callable[P, R]:
    @wraps(fn)
    def inner(*args: P.args, **kwargs: P.kwargs) -> R:
        return fn(*args, **kwargs)
    return inner

Preserves signature for type checkers.

Class decorator

def add_repr(cls):
    def __repr__(self):
        return f"{cls.__name__}({self.__dict__})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

Stacking

@cache
@validate_args
@log_calls
def expensive(x: int) -> int:
    return x ** 2

Order matters: applied bottom-up. cache wraps validate_args(log_calls(expensive)).

functools.cache / lru_cache

from functools import cache, lru_cache

@cache                    # unbounded; 3.9+
def fib(n): ...

@lru_cache(maxsize=128)
def parse(s): ...

For pure functions.

functools.partial

from functools import partial

add = lambda x, y: x + y
add5 = partial(add, 5)
add5(3)                   # 8

Context manager (with)

class DBConnection:
    def __enter__(self):
        self.conn = connect()
        return self.conn
    def __exit__(self, exc_type, exc, tb):
        self.conn.close()

with DBConnection() as conn:
    conn.execute(...)

contextmanager decorator

from contextlib import contextmanager

@contextmanager
def acquire(lock):
    lock.acquire()
    try:
        yield lock
    finally:
        lock.release()

with acquire(my_lock):
    ...

Cleaner than writing __enter__/__exit__.

Async context manager

class AsyncDB:
    async def __aenter__(self):
        self.conn = await connect()
        return self.conn
    async def __aexit__(self, exc_type, exc, tb):
        await self.conn.close()

async with AsyncDB() as conn:
    ...

asynccontextmanager

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app):
    db = await connect()
    yield db
    await db.close()

FastAPI lifespan pattern.

ExitStack (multiple contexts)

from contextlib import ExitStack

with ExitStack() as stack:
    files = [stack.enter_context(open(f)) for f in paths]
    # all files closed on exit

For dynamic number of contexts.

AsyncExitStack

from contextlib import AsyncExitStack

async with AsyncExitStack() as stack:
    conns = [await stack.enter_async_context(connect(d)) for d in dbs]

suppress

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("maybe.txt")

Cleaner than try/except for “ignore this error.”

redirect_stdout

from contextlib import redirect_stdout
import io

buf = io.StringIO()
with redirect_stdout(buf):
    print("hello")

captured = buf.getvalue()

For capturing output.

Singleton via decorator

def singleton(cls):
    instance = None
    @wraps(cls)
    def inner(*args, **kwargs):
        nonlocal instance
        if instance is None:
            instance = cls(*args, **kwargs)
        return instance
    return inner

@singleton
class Cache:
    ...

property

class User:
    @property
    def full_name(self) -> str:
        return f"{self.first} {self.last}"
    
    @full_name.setter
    def full_name(self, value: str):
        self.first, self.last = value.split(" ", 1)

staticmethod / classmethod

class Math:
    @staticmethod
    def square(x): return x ** 2
    
    @classmethod
    def from_dict(cls, d): return cls(**d)

Common mistakes

  • Forgetting @wraps — loses function metadata.
  • Mixing sync decorator on async function — silent bugs.
  • Mutable closure state without nonlocal — UnboundLocalError.
  • Forgetting cleanup in __exit__ — resource leak.

Read this next

If you want my decorator + context manager library, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .