Late Binding Closures Gotcha
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
- Prevent subtle runtime bugs in dynamically created functions
- Understand how Python's name binding differs from other languages
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
- Forgetting closures bind to names: Assuming a lambda `lambda: x` captures the value of `x` at that instant. Fix: Use a default argument like `lambda x=x: x` to force eager evaluation, capturing the value at definition time.
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
- Why do dynamically generated callbacks in a loop often end up doing the exact same thing when called?
- How can you force a closure to capture the current value of a variable in a loop?
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