Late Binding Closures Gotcha

RoadmapsPython

Overview

Python's closures bind to variables (names), not their values at the time the closure is created. When you create functions inside a loop (like a list of lambdas), they all keep a reference to the SAME loop variable — not a separate snapshot each time round. funcs = [lambda: i for i in range(3)] print(funcs[0]()) # 2, not 0! print(funcs[1]()) # 2 print(funcs[2]()) # 2 When the loop finishes, that variable holds its final value (`2`), so every closure sees that same final value when called — none of them remember `0` or `1`. Think of it like giving a group of people a link to a live Google Doc (the variable) instead of handing each person a printed PDF (the value) at different times. When they look at the doc later, they all see the latest version.

This is a very common bug when dynamically generating callbacks, event listeners, or routes in a loop, leading to all callbacks executing with the arguments intended for the final iteration.

Where used: FastAPI route generation, Tkinter UI event callbacks, Factory functions returning lambdas

Why learn this

Code walkthrough

funcs = []
for i in range(3):
  funcs.append(lambda: i)

print(funcs[0]())
print(funcs[1]())
print(funcs[2]())

Focus: funcs.append(lambda: i) # i is not evaluated here, it is looked up when the lambda is called

Aha moment

def make_printers():
  printers = []
  for i in [1, 2, 3]:
    printers.append(lambda: i)
  return printers

for p in make_printers():
  print(p())

Prediction: What does this code print?

Common guess: 1 2 3

It prints 3 3 3. The lambdas capture the variable 'i', not the value it had. By the time the lambdas are called, the loop is over and 'i' is 3.

Common mistakes

Glossary

bind
Connecting a variable name to a specific object or value in the computer's memory. e.g. `x = 42` binds the name `x` to the integer `42`.
eager evaluation
Calculating or capturing a value immediately at the exact moment the code is run, rather than waiting until it is needed. e.g. `lambda x=x: x` captures the current value of `x` at definition time.

Recall questions

Understanding checks

What will this code print?

C C

The lambda functions do not capture the value of `letter` eagerly. Instead, they capture a reference to the name `letter`. By the time the lambdas are executed, the loop has completed and `letter` is 'C'. Therefore, both callbacks print 'C'.

This code is supposed to create a list of functions that each return their respective index (0, 1, 2). However, they all return 2. How do you fix the bug?

Change the lambda to capture the value using a default argument: `lambda i=i: i`

Using `i=i` as a default argument evaluates the current value of `i` at the time the lambda is defined, effectively binding the closure to the value instead of the loop variable's name.

Practice tasks

Fix the Multiplier Factory

The function `create_multipliers` returns a list of functions where the first is supposed to multiply by 0, the second by 1, up to 4. However, currently they all multiply by 4. Fix the bug by forcing early binding.

Challenge

Dynamic Routing Handlers

You are generating a list of mock route handlers. Iterate over `endpoints = ['/home', '/about', '/contact']`. Write a list comprehension that creates a list of lambda functions, where each lambda takes no arguments and returns the string 'Handling ' followed by the endpoint. Ensure each lambda captures the correct endpoint value.

Continue learning

Previous: Closures

Return to Python Roadmap