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.
  • global exists 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.wrapsWith functools.wraps
parse.__name__ is wrapperKeeps parse
The docstring disappearsThe docstring is preserved
Debuggers and logs show wrapperShow the real function
inspect.signature is wrongReports the wrapped signature
lru_cache on a wrapped function breaksComposes 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
💡
A decorator factory needs one extra layer of nesting: the outermost function receives the configuration and returns the decorator. Without arguments, @retry would pass the decorated function straight into the times parameter and fail confusingly later.
  • @contextmanager wraps a generator whose single yield marks the boundary of the block.
  • Wrap cleanup in try/finally so it runs even if the body raises.
  • Returning True from __exit__ suppresses the exception — do that only for expected, handled conditions.
  • contextlib.suppress and ExitStack cover the common one-liners and dynamically sized sets of resources.

FAQ

How do I write a decorator that works on methods too?
Use *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?
No. The decorator runs once at import time; only the wrapper body runs per call. Keep the wrapper minimal if the function is on a hot path.

Iterators, generators and itertools Type hints and static checking

Last refreshed 2026-09-18.