Creating functions or lambdas in a loop or comprehension
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Creating functions or lambdas in a loop is valid Python, but it surprises people because closures use late binding. The function does not store the current loop value unless you bind it explicitly. If you understand that rule, the fix is straightforward and the pattern becomes safe to use.
Why Loop-Created Closures Surprise People
Consider this code:
Many people expect [0, 1, 2, 3, 4], but the output is [4, 4, 4, 4, 4]. Each lambda closes over the same variable name i, and that name is looked up when the lambda is called, not when it was created.
The same issue appears in comprehensions:
This is not a lambda-specific quirk. Nested def functions behave the same way because the rule comes from Python closure semantics.
Bind the Current Value with a Default Argument
The most common fix is to bind the current loop value into a default argument.
Now the output is [0, 1, 2, 3, 4]. The reason is that default argument expressions are evaluated when the function object is created, so each lambda gets its own stored value.
The comprehension version works the same way:
This is compact and idiomatic when the function body is short.
Use a Factory Function for Clearer Intent
For more complex logic, a small function factory is usually easier to read than a lambda with default arguments.
This pattern makes the captured value explicit and gives the inner behavior a meaningful name. It is especially useful in callback-heavy systems or test helpers where readability matters more than saving one line.
functools.partial Is Often a Better Fit
If your real goal is pre-binding arguments to an existing function, functools.partial is often cleaner than a closure.
This avoids closure mechanics entirely and communicates intent directly: you are specializing a function by fixing one argument.
Callback Code Is Where This Bug Usually Appears
Late-binding issues often stay hidden until callbacks run later than the loop that created them. That is why the bug shows up frequently in GUI code, asynchronous scheduling, and event registration.
When the function call happens long after setup, it can look as if the callback system is broken. In reality, the closure simply captured the wrong thing.
Mutable Objects Still Need Care
Binding a loop value early does not magically freeze mutable objects. If you capture a list or dictionary and then mutate it later, all closures that reference that object will see the updated state.
The default argument stores the object reference, not a deep copy. If you need a snapshot, copy the data explicitly.
Choose the Most Obvious Binding Style
The best approach depends on the situation:
- default arguments for small local closures
- factory functions for more complex behavior
- '
partialfor argument pre-binding to existing functions'
The right goal is not showing that you know closure tricks. The right goal is writing code where the binding behavior is obvious to the next reader.
Common Pitfalls
- Expecting loop-created lambdas to capture the current value automatically.
- Forgetting that nested
deffunctions follow the same late-binding rule. - Using clever one-liners when a small factory function would be clearer.
- Assuming default arguments freeze mutable objects instead of storing references.
- Debugging the callback system instead of checking closure binding semantics first.
Summary
- Closures created in loops use late binding by default.
- Use
lambda i=i: ...when you need to bind the current loop value immediately. - Use factory functions when the closure body deserves a name or more structure.
- Use
functools.partialwhen the real goal is argument pre-binding. - Be extra careful with mutable captured objects and delayed callbacks.

