skip to content

Why does every lambda in [lambda: i for i in range(3)] return 2?

level: juniorimportance: must knowfreq 68%

answer

  1. Ask when the value is read
  2. Closures capture names, not snapshots
  3. The lambdas all run after the loop
  4. Default parameters evaluate at creation time
  5. lambda i=i freezes this iteration

basics

~20 s

Each lambda captures the variable i itself, not a snapshot of its value at creation time. The lambdas are called after the comprehension has finished, when i holds its final value 2, so all three return 2.

solid answer

~50 s

Python closures are **late-binding**: a nested function keeps a reference to the enclosing variable and reads it *when it is called*, not when it is defined. The comprehension creates three separate function objects without executing any of their bodies, and its iteration variable ends at 2. Calling them afterwards makes all three resolve the same `i` and get 2. An explicit `for` loop that appends lambdas to a list behaves identically. The fix is to bind the value eagerly at creation time: give the lambda a default parameter, `[lambda i=i: i for i in range(3)]`, because default values are evaluated once when the lambda is created; or freeze the argument with `functools.partial`; or call a factory function that takes the value as a parameter and returns the inner function. All three produce 0, 1, 2.

code

pycon · 6 lines
pycon
>>> funcs = [lambda: i for i in range(3)]
>>> [f() for f in funcs]
[2, 2, 2]
>>> fixed = [lambda i=i: i for i in range(3)]
>>> [f() for f in fixed]
[0, 1, 2]

go deeper

for a junior

Be ready to predict the output of functions built in a loop and say, in one sentence, why they all report the last value. Knowing the lambda i=i fix by heart is enough at this level.

for a middle

Explain the two separate timings: the name is resolved when the function runs, while a default parameter is evaluated when the function is created. Be able to write both the broken and the fixed version from memory.

for a senior

Show where this bites in real systems — handlers, dispatch tables and deferred work built in a loop — and describe how you spot it in review rather than only in a REPL demo.

for a principal

Own the guidance: prefer registration helpers that take the per-iteration value as a parameter so eager binding is structural, and decide whether a lint rule for loop-variable capture earns its false positives in your codebase.

**The rule in one line: a nested function stores a reference to the variable it captured, and reads that variable when the function is called.** Python calls this *late binding*. Nothing about `lambda`, comprehensions or lists is special here — the same rule governs every function defined inside another scope. ### What the comprehension actually builds `[lambda: i for i in range(3)]` runs three iterations. Each iteration evaluates the `lambda` expression, and evaluating a `lambda` **creates a new function object**; it does not execute the body. The body is just the name `i`, and nothing looks it up yet. After three iterations you hold three distinct function objects, and the comprehension's iteration variable has finished at 2. Then you call them. Each call executes the body. `i` is not a parameter of the lambda and is not assigned inside it, so it is a *free variable*: it is resolved in the scope that enclosed the lambda when it was created, and that scope's `i` now holds 2. Three separate function objects, one shared variable, one answer: `[2, 2, 2]`. Two things are commonly confused here, and separating them is most of the answer: 1. **Which** variable a nested function reads is decided when the code is compiled, from where the name is bound. 2. **What value** that variable holds is decided when the function runs. Late binding is entirely about (2). ### The same trap without a comprehension ```python funcs = [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) # [2, 2, 2] ``` Identical output. The only difference between the loop and the comprehension is scope hygiene: after a `for` loop the loop variable is still bound in the enclosing function, so `print(i)` shows 2; a comprehension keeps its variable to itself, so you cannot see it afterwards — even though the lambdas can. A tiny example makes the timing obvious without any loop at all: ```python def build(): x = 1 f = lambda: x x = 99 return f print(build()()) # 99 ``` The function was created while `x` was 1 and still reports 99, because the read happens at call time. ### The fixes: one new binding per iteration Every working version does the same thing — gives each callable a binding created during that iteration. Python creates a fresh binding on a *call*, in the form of parameters, so all three standard fixes are ways of turning the loop value into a parameter. * **Default parameter.** `[lambda i=i: i for i in range(3)]` yields `[0, 1, 2]`. A default value is evaluated once, when the `lambda` expression executes, so each object stores that iteration's value; inside the body the parameter shadows the enclosing name. The cost is a public parameter that a caller can override. * **`functools.partial`.** `partial(check, i)` records `i` when the partial object is created and supplies it when the object is called. The underlying function stays an ordinary named function taking the value as a parameter. * **A factory function.** An outer function that takes the value as a parameter and returns the inner function. The most explicit, and the only one that still reads well when the inner function is more than a single expression. What does *not* work is rebinding to another name inside the loop: `j = i` creates another variable in the same scope, and it moves on with the loop exactly as `i` does. ### Where it actually bites Almost never in a REPL, where you call the function immediately and it happens to be right. It bites when the calls are **deferred**: handlers registered in a loop, a dispatch table built from a list of names, wrappers applied per item, work items queued for later. None of them raise; they all "work"; they simply all agree, and they agree on the last item. That is why the real-world symptom is usually every consumer reading one wrong value rather than a traceback. ### Version notes Python 3 gave comprehensions their own scope, which is why the iteration variable does not leak into the surrounding function (Python 2's list comprehensions leaked it). Python 3.12 inlined comprehensions as an optimization (PEP 709) while preserving those scoping semantics, and 3.14 behaves the same way: lambdas built in a comprehension still read the iteration variable at call time and still return `2, 2, 2`. ### What to say in an interview Predict the output, state the rule, name at least two fixes, and say where you have hit it in real code. One sentence shows you understand it rather than memorized it: *the function captured the variable, not the value, and the variable is read when the function runs.*

  • Does an explicit for loop behave differently from the comprehension here?
    No. In both cases each function reads the iteration variable when it is called, so all of them see the final value. The one visible difference is scope: a `for` loop's variable stays bound in the enclosing function after the loop ends, while a comprehension keeps its variable to itself. The captured value is the last one either way.
  • Why does writing lambda i=i: i fix it?
    A default parameter value is evaluated once, at the moment the `lambda` expression executes — which inside a loop means once per iteration, with the value that iteration has. That value is stored on the function object, and the parameter shadows the enclosing name inside the body, so the closure lookup never happens. The cost is a public parameter any caller can override.
  • What do you get if you call each lambda inside the loop instead of after it?
    0, 1 and 2. Late binding only bites because the calls happen after the variable has moved on; calling a function in the same iteration that created it reads the variable while it still holds that iteration's value. That is exactly why the bug hides in callbacks, dispatch tables and any work that runs later.

The lambda is a note saying "go read i off the whiteboard", not a photocopy of what the whiteboard said when the note was written. By the time anyone reads the notes, the whiteboard shows the last value.

saying these in an interview costs you the question

  • Says the lambda copies the value of i when it is defined
  • Calls it a bug in comprehensions or in lambda itself
  • Thinks each lambda gets its own separate copy of i
  • Blames the list and tries to fix it by copying the list
  • Claims a nested def instead of a lambda would avoid it
  • Says calling list() on the comprehension changes the result

context