Decorators & Context Managers
Functions Are Values
Python functions can be assigned, passed as arguments, and returned from other functions. A closure is an inner function that remembers variables from its enclosing scope. Decorators are built on exactly these two ideas.
What a Decorator Is
A decorator takes a function and returns a replacement — usually a wrapper that adds behaviour around the original:
import functools, time
def timing(func):
@functools.wraps(func) # keeps func's name/docs intact
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
ms = (time.perf_counter() - start) * 1000
print(f"{func.__name__} took {ms:.1f}ms")
return result
return wrapper
@timing
def slow_report():
time.sleep(0.5)
return "done"
slow_report() # slow_report took 500.4ms@timing is just syntax sugar for slow_report = timing(slow_report). Always use functools.wraps — without it, the wrapped function loses its identity.
Decorators With Arguments
Add one more layer — a factory that returns the decorator:
def retry(times=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception:
if attempt == times:
raise
print(f"attempt {attempt} failed, retrying…")
return wrapper
return decorator
@retry(times=5)
def fetch_data():
...You’ve already met real decorators: Django’s @login_required, pytest’s @pytest.fixture, @property, @dataclass.
Context Managers — the with Protocol
with works on any object that defines __enter__ (setup) and __exit__ (guaranteed teardown):
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # bound to the `as` name
def __exit__(self, exc_type, exc, tb):
self.ms = (time.perf_counter() - self.start) * 1000
print(f"block took {self.ms:.1f}ms")
return False # don't suppress exceptions
with Timer():
do_heavy_work()The Easy Way: @contextmanager
One generator function: everything before yield is setup, everything after is teardown:
from contextlib import contextmanager
@contextmanager
def db_transaction(conn):
conn.begin()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
with db_transaction(conn) as tx:
tx.execute("UPDATE ...") # commits on success, rolls back on errorWhenever you write setup/teardown pairs — open/close, lock/unlock, start/stop — reach for a context manager.