Decorators, closures and context managers
Closures and late binding, decorators written by hand with functools.wraps, decorator factories, and custom with-blocks via contextlib.
Closures and late binding
A closure is a function that remembers names from the enclosing scope after that scope has returned. Python stores the captured variables in a cell, so the inner function sees the current value, not a snapshot.
def make_counter(start=0):
count = start
def step(by=1):
nonlocal count # rebind the enclosing variable
count += by
return count
return step
tick = make_counter(10)
tick() # 11
tick(5) # 16
# late binding: every function shares the same variable
funcs = [lambda: n for n in range(3)]
print([f() for f in funcs]) # [2, 2, 2] - not [0, 1, 2]
# bind the current value with a default argument
funcs = [lambda n=n: n for n in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
# inspect what a closure captured
print(tick.__closure__[0].cell_contents) # 16- Reading an enclosing variable needs nothing; rebinding it needs
nonlocal. globalexists for the same reason but should be rare in application code.- A closure over a loop variable is the single most common source of the late-binding bug in callbacks and handlers.
Writing a decorator
A decorator is a function that takes a function and returns a replacement. The @name line is pure syntax sugar: @dec above def f means f = dec(f).
import functools, logging, time
def timed(fn):
@functools.wraps(fn) # copies __name__, __doc__, __wrapped__
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
logging.info("%s took %.3fs", fn.__name__, elapsed)
return wrapper
@timed
def parse(path):
"""Read and parse a file."""
return [line.split(",") for line in open(path, encoding="utf-8")]
parse("data.csv")
print(parse.__name__, parse.__doc__) # parse Read and parse a file.
# stacking: the bottom decorator is applied first
@timed
@functools.lru_cache(maxsize=128)
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)| Without functools.wraps | With functools.wraps |
|---|---|
parse.__name__ is wrapper | Keeps parse |
| The docstring disappears | The docstring is preserved |
Debuggers and logs show wrapper | Show the real function |
inspect.signature is wrong | Reports the wrapped signature |
lru_cache on a wrapped function breaks | Composes correctly |
Preserve the return value and re-raise exceptions unchanged. A wrapper that swallows errors or returns None on failure quietly changes the contract of every function it decorates.
Decorators with arguments and custom with-blocks
To pass configuration to a decorator, add one more layer: the outer function takes the arguments and returns the actual decorator.
import functools, time
def retry(times=3, delay=0.1, exceptions=(OSError,)):
def decorate(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return fn(*args, **kwargs)
except exceptions as err:
last = err
time.sleep(delay * attempt) # simple backoff
raise last
return wrapper
return decorate
@retry(times=5, delay=0.2)
def fetch(url):
...
from contextlib import contextmanager
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield # body of the with-block runs here
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
with timer("parse"):
parse("data.csv")
# a class-based context manager, when you need state or reuse
class Session:
def __enter__(self):
self.conn = connect()
return self.conn
def __exit__(self, exc_type, exc, tb):
self.conn.close()
return False # False: do not swallow the exception@retry would pass the decorated function straight into the times parameter and fail confusingly later.@contextmanagerwraps a generator whose singleyieldmarks the boundary of the block.- Wrap cleanup in
try/finallyso it runs even if the body raises. - Returning
Truefrom__exit__suppresses the exception — do that only for expected, handled conditions. contextlib.suppressandExitStackcover the common one-liners and dynamically sized sets of resources.
FAQ
How do I write a decorator that works on methods too?
*args, **kwargs in the wrapper and keep self as the first positional argument. Do not add self to the wrapper signature — it arrives inside args.Are decorators expensive?
Related
Iterators, generators and itertools Type hints and static checking
Last refreshed 2026-09-18.