Late-binding closures
What is a closure
A closure is a nested function that captures and remembers variables from its enclosing scope, even after that scope has finished executing. The inner function keeps the outer function’s locals alive.
def multiplier(n):
def multiply(x): # inner function reads `n` from the enclosing scope
return x * n # `n` is a *free variable* — not a param, not a global
return multiply # returned with `n` still bound
times3 = multiplier(3)
times3(10) # 30 — `n=3` survived after multiplier() returned
Three ingredients: a nested function, a free variable it reads from the enclosing scope, and the outer function returning the inner one. Python stores each captured variable in a cell, visible via __closure__:
times3.__closure__[0].cell_contents # 3
Closures are how decorators, factories, and callbacks carry state without a class. The catch is how they capture — by variable, not by value — which is the gotcha below.
The gotcha
Closures capture variables, not values. The lookup happens when the closure is called, not when it’s created.
Minimal repro
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs]) # [2, 2, 2] ← all capture the same `i`
The classic loop-variable trap. Same with for i in range(3): funcs.append(lambda: i).
Why it happens
Each lambda has a closure over the enclosing scope’s name i. By the time the lambdas run, the loop has finished and i is bound to the last value, 2. Python doesn’t snapshot i at lambda creation.
How to avoid
Pin via default argument (default args ARE evaluated eagerly, see 01_mutable_default_arguments.md):
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs]) # [0, 1, 2]
Or use functools.partial:
from functools import partial
funcs = [partial(lambda x: x, i) for i in range(3)]
Or wrap in an immediate function:
def make(i):
return lambda: i
funcs = [make(i) for i in range(3)]
Interview angle
Almost universal. Often paired with event handlers — for btn in buttons: btn.on_click(lambda: handle(btn)) is broken in the same way.