What is a closure, and what's the late-binding pitfall with closures in loops?
Quick Answer
A closure is a nested function that **remembers variables from its enclosing scope** even after that scope has finished executing, by keeping a reference to the enclosing variable (a "cell"), not a snapshot of its value at creation time. The classic pitfall: closures created in a loop (e.g., a list of lambdas) all share the **same** loop variable, so by the time they're called, they all see its *final* value — not the value at the time each closure was created.
Detailed Answer
What makes something a closure
def make_multiplier(factor):
def multiply(x):
return x * factor # `factor` is captured from the enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
multiply is a closure over factor — even after make_multiplier
returns, multiply still has access to its own factor, because Python
keeps the enclosing variable alive as long as any closure references it.
double and triple each capture a different factor because each
call to make_multiplier creates a fresh scope.
The late-binding trap
funcs = []
for i in range(3):
funcs.append(lambda: i)
[f() for f in funcs] # [2, 2, 2] -- NOT [0, 1, 2]!
Every lambda captures the variable i by reference, not its value at
the time the lambda was created. Since all three lambdas share the same
enclosing scope (the loop body doesn't create a new scope per iteration —
loops don't introduce scopes in Python at all), by the time they're
called, i has already finished the loop and holds its final value, 2.
The fix: bind the value via a default argument
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # default arg is evaluated NOW, at def-time
[f() for f in funcs] # [0, 1, 2]
Default argument values are evaluated once, when the lambda/def is
created — using i=i copies the current value of i into the
function's own default, decoupling it from the loop variable's later
mutations.
Alternative fix: a factory function
def make_getter(value):
return lambda: value # closes over `value`, a fresh parameter per call
funcs = [make_getter(i) for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
Each call to make_getter creates a genuinely new scope with its own
value, so each returned lambda captures a distinct variable — this is
the same principle as make_multiplier above, just applied to fix the
loop pitfall.
Interview-ready summary: Closures capture variables by reference to the enclosing scope, not by value — which is powerful for building factories/decorators but means closures created in a loop all share the loop variable and see its final value unless you force early binding (default argument trick or a separate factory function per iteration).