Python's Late-Binding Closure Trap

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.

The Bug

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:

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).

Key Takeaway: Python closures capture variables by reference, not by value — if you create closures in a loop, they all share the loop variable's final value unless you force early binding with a default argument or a factory function.

All newsletters