Python lambda in loop: closure capture and fixes explained
Why do Python lambdas in a loop capture the final value? Learn Python closures (late binding), the lambda default-argument fix, and recommended lazy-initialization patterns.
Why does this lazy initialization using a lambda capture the loop variable’s final value instead of the current one? I expected tmp[0].get(“value”) to print 1 but it prints 3. Example:
class Lazy:
def __init__(self, factory):
self._factory = factory
self._value = None
def _init(self):
if self._value is None:
self._value = self._factory()
def __getattr__(self, name):
self._init()
return getattr(self._value, name)
tmp = []
for el in [1, 2, 3]:
tmp.append(
Lazy(lambda : {"value":el})
)
print(tmp[0].get("value"))
The workaround below works (binding el as a default argument):
class Lazy:
def __init__(self, factory):
self._factory = factory
self._value = None
def _init(self):
if self._value is None:
self._value = self._factory()
def __getattr__(self, name):
self._init()
return getattr(self._value, name)
tmp = []
for el in [1, 2, 3]:
tmp.append(
Lazy(lambda el=el : {"value":el})
)
print(tmp[0].get("value"))
If the loop variable el were a reference to the list element, I would expect this to modify the list:
tmp = [1, 2, 3]
for el in tmp:
el = 1
print(tmp)
but it prints [1, 2, 3]. How do Python’s variable binding and closure capture semantics explain these behaviors (why the first lambda sees 3, why the default-argument trick works, and why assigning to el inside the loop doesn’t modify the list)? What’s the recommended way to implement lazy initialization in this pattern so each Lazy instance captures the current loop value?
Python closures (lambda python) capture names from the enclosing scope, not the immediate values; your loop-created lambda closes over the single loop variable el, which is looked up when the lambda runs—after the loop finishes and el == 3. Binding the current value at creation time (for example lambda el=el: ...), using a small factory function, or functools.partial snapshots the value so each Lazy instance gets the intended value. For lazy initialization, create the factory with the value bound at construction (default arg, factory, or a classmethod wrapper) so each Lazy captures the per-iteration value.
Contents
- lambda python and python closure semantics
- Late binding in loops — why the first lambda sees 3
- Why the default-argument trick works and alternatives
- Why assigning to the loop variable doesn’t change the list
- Recommended lazy-initialization patterns (capture value per instance)
- Sources
- Conclusion
lambda python and python closure semantics
A closure is a function that remembers names from its enclosing scope. In Python, that “remembering” records the variable (the name), not a snapshot of its value. That means a lambda defined inside a loop will keep a reference to the loop variable name; when you call the lambda later, Python looks up the name and uses whatever value it currently holds (this is called late binding).
Short example (minimal reproduction):
fns = []
for x in [1, 2, 3]:
fns.append(lambda: x)
print([fn() for fn in fns]) # -> [3, 3, 3]
All three lambdas return 3 because they all close over the same name x, and x is 3 once the loop finishes. This behavior and the usual fixes are explained in-depth in the Real Python guide on lambdas and in several community answers such as the Stack Overflow thread on the same problem (see Sources). See also the explanation of closures for background on how enclosed names are captured and looked up at call time: https://realpython.com/python-lambda/ and https://stackoverflow.com/questions/33983980/lambda-in-for-loop-only-takes-last-value.
Late binding in loops — why the first lambda sees 3
Your original code effectively does this:
class Lazy:
def __init__(self, factory):
self._factory = factory
self._value = None
def _init(self):
if self._value is None:
self._value = self._factory()
def __getattr__(self, name):
self._init()
return getattr(self._value, name)
tmp = []
for el in [1, 2, 3]:
tmp.append(Lazy(lambda: {"value": el}))
print(tmp[0].get("value")) # prints 3, not 1
Why? Because each lambda references the name el. The loop rebinds that name each iteration; it isn’t “copied” into the closure at lambda creation time. When tmp[0].get("value") runs, the lambda executes and looks up el (which is now 3). This is the classic late-binding closure gotcha; community explanations and examples are on Stack Overflow and GeeksforGeeks (see Sources).
Why the default-argument trick works and alternatives
Default arguments are evaluated at function definition time. So when you write lambda el=el: ..., the right-hand el is evaluated immediately and stored as the default value. The lambda then has a local default that holds the snapshot.
Fixed example (your working workaround):
tmp = []
for el in [1, 2, 3]:
tmp.append(Lazy(lambda el=el: {"value": el}))
print(tmp[0].get("value")) # prints 1
Alternatives that also snapshot the current value:
- Factory function (explicit and clear):
def make_factory(value):
def factory():
return {"value": value}
return factory
tmp = []
for el in [1, 2, 3]:
tmp.append(Lazy(make_factory(el)))
- functools.partial (binds arguments into a callable):
from functools import partial
tmp = []
for el in [1, 2, 3]:
tmp.append(Lazy(partial(lambda v: {"value": v}, el)))
All three approaches evaluate and store el at the time you create the factory/callable. The GeeksforGeeks write-up and Real Python article describe these techniques in detail (see Sources): https://www.geeksforgeeks.org/python/why-do-python-lambda-defined-in-a-loop-with-different-values-all-return-the-same-result/ and https://realpython.com/python-lambda/.
Why assigning to the loop variable doesn’t change the list
You asked why this doesn’t change the list:
tmp = [1, 2, 3]
for el in tmp:
el = 1
print(tmp) # still [1, 2, 3]
Simple: for el in tmp: binds the name el to each element value in turn. Doing el = 1 rebinds the local name el to a new object; it does not write back into the list. Names and container slots are different. If you want to replace items in the list, assign by index:
for i in range(len(tmp)):
tmp[i] = 1
Or, if list elements are mutable objects, mutating the object via the name will change the list contents because both the name and the list slot reference the same object:
tmp = [[1], [2], [3]]
for el in tmp:
el.append(9)
print(tmp) # [[1, 9], [2, 9], [3, 9]]
This distinction—rebinding a name vs. mutating the referenced object—is fundamental to understanding Python’s reference semantics. The gotcha is closely related to the closure behavior explained earlier (names vs. values)—see the scoping/closures discussion in the Programiz and eev.ee posts for more background (see Sources).
Recommended lazy-initialization patterns (capture value per instance)
Which approach should you use in real code? Pick the one that balances clarity and brevity for your team:
- Default-argument snapshot (concise, idiomatic)
- Pros: shortest, widely used.
- Cons: occasionally cryptic to beginners.
- Example:
tmp.append(Lazy(lambda el=el: {"value": el}))
- Explicit factory function (clear and expressive)
- Pros: very readable, easy to debug.
- Example:
def make_factory(value):
return lambda: {"value": value}
for el in [1,2,3]:
tmp.append(Lazy(make_factory(el)))
- functools.partial (good when you already have a function signature)
- Example:
from functools import partial
tmp.append(Lazy(partial(lambda v: {"value": v}, el)))
- Provide a small helper on Lazy for clarity
- Example:
class Lazy:
def __init__(self, factory):
self._factory = factory
self._value = None
@classmethod
def from_value(cls, value):
return cls(lambda value=value: value)
def _init(self):
if self._value is None:
self._value = self._factory()
def __getattr__(self, name):
self._init()
return getattr(self._value, name)
# Usage:
for el in [1,2,3]:
tmp.append(Lazy.from_value({"value": el}))
Which one is “recommended”? For readability and maintainability I usually prefer an explicit factory or a small from_value constructor—it’s obvious what’s being captured. The default-argument idiom is fine and idiomatic in compact code, but if people on your team find it mysterious, pick the factory pattern.
Also note: default-argument binding stores a reference to the object. If the captured object is mutable and later mutated elsewhere, callbacks will observe that mutation; snapshot semantics are about binding the reference at creation time, not making a deep copy.
If you want automatic per-attribute lazy initialization in classes, consider functools.cached_property (for attributes on an object) rather than rolling a bespoke Lazy wrapper.
Sources
- https://stackoverflow.com/questions/33983980/lambda-in-for-loop-only-takes-last-value
- https://www.geeksforgeeks.org/python/why-do-python-lambda-defined-in-a-loop-with-different-values-all-return-the-same-result/
- https://realpython.com/python-lambda/
- https://stackoverflow.com/questions/54288926/python-loops-and-closures
- https://www.programiz.com/python-programming/closure
- https://www.pythontutorial.net/advanced-python/python-closures/
- https://eev.ee/blog/2011/04/24/gotcha-python-scoping-closures/
- https://codeql.github.com/codeql-query-help/python/py-loop-variable-capture/
- https://ru.stackoverflow.com/questions/1266287/lambda-функция-в-генераторе-цикле-неверный-подсчет-значения
- https://marketello.org/lesson/лямбда-функции-в-цикле/
Conclusion
The root cause is late binding: Python closures (python closure) capture the loop variable name, not its instantaneous value, so every lambda you made in the loop sees the final value. The pragmatic fixes are to bind the value when you create the factory—use lambda el=el: ..., an explicit factory function, or functools.partial—or add a small from_value helper on your Lazy class. Pick the pattern that makes your intent clearest to future readers.