2026-04-27
A developer is building a simple callback system. They want to create a list of functions, where each function returns the square of its index. They test it and get baffling results:
def make_power_functions(n):
"""Create n functions where func[i] returns i squared."""
functions = []
for i in range(n):
functions.append(lambda: i ** 2)
return functions
# Usage
squares = make_power_functions(5)
# Expected: 0, 1, 4, 9, 16
for fn in squares:
print(fn())
The output is not 0, 1, 4, 9, 16. Instead, every single call prints 16.
This is Python's late-binding closure problem. The lambda lambda: i ** 2 does not capture the value of i at the time the lambda is created. It captures a reference to the variable i in the enclosing scope. By the time any of these lambdas are actually called, the loop has finished and i is stuck at its final value: 4. So every function computes 4 ** 2 = 16.
This isn't a Python quirk in the pejorative sense — it's consistent with how closures work in Python. Closures close over variables, not values. The variable i lives in the enclosing function's scope, and all lambdas share that same binding.
What makes this bug especially insidious:
i happens to be 0.i advances), it appears to work, masking the bug during naive testing.The fix is to force early binding by using a default argument, which is evaluated at function definition time:
def make_power_functions(n):
"""Create n functions where func[i] returns i squared."""
functions = []
for i in range(n):
functions.append(lambda i=i: i ** 2)
return functions
# Now correctly outputs: 0, 1, 4, 9, 16
squares = make_power_functions(5)
for fn in squares:
print(fn())
The i=i default argument creates a new local variable in each lambda's own scope, bound to the current value of the outer i at creation time. Alternatively, you can use functools.partial or a factory function:
def make_power_functions(n):
def make_fn(x):
return lambda: x ** 2
return [make_fn(i) for i in range(n)]
This version is arguably clearer: make_fn creates a new scope for each call, so each lambda captures its own distinct x.
This same bug appears in JavaScript (pre-let era with var), and in any language where closures capture variables rather than values. It's particularly common in GUI programming (creating button callbacks in a loop) and in async code (scheduling tasks with loop-dependent parameters).
