← Python Code Python's Hidden Traps
Browse Python Concepts

Late Binding Closures — Why lambda in a Loop Breaks

Mental Model

Python closures are like a camera pointed at a variable name, not at its value. When you later look at the photo (execute the function), you see whatever is in front of the camera now (the current value of the variable), not what was there when you pressed the shutter.

Rule: When creating closures or lambdas inside a loop, always force early binding by passing the loop variable as a default parameter.

The Setup

You are creating a routing table for an event dispatcher that registers several callback functions dynamically using a loop. Each callback is supposed to multiply its input by its index in the registration loop. During testing, you notice every single callback behaves identically.

What Does This Print?

Broken code
Python
callbacks = []
for i in range(3):
    callbacks.append(lambda x: x * i)

print([f(10) for f in callbacks])
Predict what the list comprehension outputs. Will it print the expected series [0, 10, 20], or will it output something else?

The Output

What actually happens
[20, 20, 20]

All three generated lambda functions output 20 because they all reference the exact same variable i from the surrounding scope. By the time the lambdas are executed in the list comprehension, the loop has completed and the value of i remains stuck at its final value: 2.

Why Python Does This

Python's closures capture variables by reference, not by value. When the compiler encounters the lambda, it notes that i is a free variable belonging to the outer scope (following the LEGB rule). It compiles the bytecode to look up i dynamically in the enclosing namespace using LOAD_DEREF when the function is eventually called. Because the loop modifies i in-place within the same frame, all lambdas look up the exact same cell object, which contains the final value 2 by the time execution begins.

The Fix

Corrected pattern
Python
callbacks = []
for i in range(3):
    # Bind the current value of i as a default argument, which is evaluated at definition time
    callbacks.append(lambda x, bound_i=i: x * bound_i)

print([f(10) for f in callbacks])

By passing the loop variable as a default argument to the lambda, its value is immediately bound to the function's scope at the time of definition (early binding). This creates a distinct copy of the variable's value for each lambda, preventing them from all referencing the same final variable.

How This Fails in Real Systems

A microservices health-check scheduler registered dynamic TCP polling tasks inside a loop. Due to late-binding closures, every single background task pinged only the very last host in the configuration file. The company remained unaware that 9 out of 10 microservices were down for over 4 hours because the dashboard reported the last healthy service repeatedly.

Key Takeaway

When creating closures or lambdas inside a loop, always force early binding by passing the loop variable as a default parameter.
Common mistake: Developers creating functions (especially lambdas) inside loops expect the loop variable's value at the time of function creation to be captured, not its final value.