Closures & Scope Capture
A closure is a function that remembers variables from the enclosing scope where it was defined. Closures enable factories, decorators, and callbacks - but late binding in loops is a classic footgun.
Search across all documentation pages
A closure is a function that remembers variables from the enclosing scope where it was defined. Closures enable factories, decorators, and callbacks - but late binding in loops is a classic footgun.
def make_multiplier(factor: int):
def multiply(value: int) -> int:
return value * factor
return multiply
double = make_multiplier(2)
print(double(10)) # 20When to reach for this:
from typing import Callable
def make_counter(start: int = 0) -> Callable[[], int]:
count = start
def inc() -> int:
nonlocal count
count += 1
return count
return inc
def make_handlers_bad():
return [lambda: i for i in range(3)] # late binding bug
def make_handlers_good():
return [lambda i=i: i for i in range(3)]
def tracer(label: str):
def decorate(fn):
def wrapper(*args, **kwargs):
print(f"{label}: enter")
return fn(*args, **kwargs)
return wrapper
return decorate
@tracer("api")
def ping() -> str:
return "pong"
if __name__ == "__main__":
c = make_counter(10)
print(c(), c())
print([h() for h in make_handlers_bad()])
print([h() for h in make_handlers_good()])
print(ping())What this demonstrates:
nonlocal mutates count in enclosing functioni - all see final value without default arg tricki=i captures value at lambda definition timetracer returns decorator closing over label| Pattern | Code |
|---|---|
| Default arg | lambda i=i: i |
| functools.partial | partial(fn, i) |
| Factory function | def make(i): return lambda: i |
def make():
funcs = []
for i in range(3):
def f(i=i):
return i
funcs.append(f)
return funcsnonlocal or global explicit.__closure__ when puzzled.| Alternative | Use When | Don't Use When |
|---|---|---|
functools.partial | Simple arg binding | Need nonlocal mutable state |
class with __call__ | Stateful functor | One-shot simple closure |
| lambda | Tiny one-liners | Multi-statement logic |
| default arg on def | Loop factory pattern | Already using partial |
Function plus captured environment of free variables from enclosing scope.
All closures reference same variable i evaluated at call time - use i=i default.
Reassigning enclosing variable - reading only needs closure without nonlocal.
Closure captures specific enclosing function scope; global module-level names.
func.__closure__ tuple of cells; cell.cell_contents for value debugging.
Outer function takes config, returns decorator closing over config, returns wrapper.
Same mutable default trap as normal functions - fresh object per call in factory body.
Small overhead vs plain function - negligible for typical callbacks.
Nested async def closes over same rules - late binding applies to async lambdas too (rare).
Return Callable[[int], int] from factory for mypy consumers.
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.
Reviewed by Chris St. John·Last updated Jul 19, 2026