What do lambda function closures capture?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In Python, lambda closures capture variables from the surrounding scope, not frozen copies of the values at the time the lambda is written. That detail is the source of a very common surprise: a group of lambdas created in a loop often all seem to “remember” the same final value.
What a Closure Really Captures
A closure keeps access to names from an enclosing lexical scope after that scope would otherwise be gone. In Python, the captured name is resolved when the function runs, not when it is defined. This is often described as late binding.
That means the lambda closes over the variable itself, not over a snapshot value.
Many beginners expect [0, 1, 2], but the actual result is [2, 2, 2]. All three lambdas refer to the same name i, and by the time they run, the loop has finished with i == 2.
Why This Happens
The closure points at the binding in the outer scope. There is only one loop variable i, and it changes on each iteration. The lambdas do not each get their own private copy automatically.
This behavior is not specific to lambda. A nested def behaves the same way.
Again, the result is [2, 2, 2].
How to Capture the Current Value Intentionally
The usual fix is to bind the current value into a default argument. Default arguments are evaluated at function definition time, so they create the snapshot behavior people often expected from the closure itself.
Now the result is [0, 1, 2] because each lambda receives its own default value.
You can do the same thing with a nested function.
This factory-function style is often clearer in larger programs.
What This Means in Real Code
The bug appears often in GUI callbacks, asynchronous scheduling, and test generation. For example, if you create button handlers in a loop and each handler references the loop variable directly, every button may behave as if it were the last one.
Here the default argument pattern keeps each handler tied to the intended value.
Closure Capture Versus Mutation
Another subtle point is that closures can observe later mutation of captured objects. If a closure captures a list and that list changes, the closure sees the changed list because it still refers to the same object.
This prints ['x'] because the closure refers to the items object, and that object was mutated after the lambda was created.
So there are really two levels to remember:
- the closure captures the binding, not an automatic copy
- the referenced object may itself be mutable
Common Pitfalls
The most common mistake is creating lambdas in a loop and expecting each one to remember the current loop value automatically.
Another frequent issue is thinking that the behavior is unique to lambda. It is really a closure rule in Python and applies to nested functions generally.
Developers also sometimes “fix” the problem by copying more data than necessary. Often a simple default argument is enough.
Finally, if the captured object is mutable, remember that future mutations are still visible through the closure unless you explicitly copy the object yourself.
Summary
- Python closures capture surrounding names, not automatic value snapshots.
- Resolution is late-bound, so loop-created lambdas often all see the final loop variable value.
- Use default arguments such as
lambda i=i: ito capture the current value intentionally. - The same rule applies to nested
deffunctions, not just lambdas. - If a captured object is mutable, later changes to that object remain visible through the closure.

