skip to content

How do you make a lambda created inside a for loop capture that iteration's value?

level: middleimportance: should knowfreq 52%

answer

  1. Each callable needs its own binding
  2. Python creates bindings on a call
  3. Turn the loop value into a parameter
  4. Default, partial, or factory function
  5. Syntax choice is not the fix

basics

~20 s

Bind the value when the function is created rather than letting it be read at call time. Three ways do that: a default parameter (lambda v=v: ...), functools.partial, or a factory function that takes the value as a parameter.

solid answer

~50 s

All three fixes do the same thing — give each callable a binding created during that iteration — and they differ in what they leave behind. A default parameter, `[lambda v=v: v for v in values]`, is the shortest: defaults are evaluated once when the `lambda` executes, so each object carries its own value in `__defaults__`. The cost is a public parameter a caller can override, and it is unusable when the callable must match a fixed signature. `functools.partial(handler, v)` binds the value eagerly without inventing a default, and keeps the real work in a normal named function that can be tested on its own. A factory — an outer function taking `v` as a parameter and returning the inner function — is the most explicit and reads best for multi-line bodies. Use the default trick for throwaway lambdas, partial or a factory for anything you register.

code

python · 17 lines
python
from functools import partial

def scale(v):
    return v * 2

def make_handler(v):
    def handler():
        return v * 2
    return handler

by_default = [lambda v=v: v * 2 for v in range(3)]
by_partial = [partial(scale, v) for v in range(3)]
by_factory = [make_handler(v) for v in range(3)]

print([h() for h in by_default])   # [0, 2, 4]
print([h() for h in by_partial])   # [0, 2, 4]
print([h() for h in by_factory])   # [0, 2, 4]

go deeper

for a junior

Memorize one working fix and be able to type it: a default parameter such as lambda v=v: v gives each function its own value. Recognizing the broken form in a code sample matters more than listing all three fixes.

for a middle

Explain why each fix works in terms of when the value is recorded, and write all three from memory. An interviewer expects you to say that a parameter is the per-iteration binding Python gives you.

for a senior

Argue the tradeoffs: a default parameter is public and overridable, functools.partial keeps the signature honest and the function testable, a factory is clearest for multi-line bodies. Say which you would accept in review and why.

for a principal

Push the fix into the API. Registration functions that take the per-iteration value as an argument make eager binding structural, and that is a cheaper guarantee across many teams than remembering an idiom.

The question is really "how do I create a **per-iteration binding**?", because that is the only thing that separates the working versions from the broken one. A function defined in a loop reads its free variables when it is called; the loop variable is a single variable that keeps moving, so every callable sees the same final value. Python hands out fresh bindings on a **call**, in the shape of parameters — so every fix is a way of making the loop value a parameter of something. ### 1. A default parameter ```python handlers = [lambda v=v: v * 2 for v in range(3)] print([h() for h in handlers]) # [0, 2, 4] ``` Default values are evaluated **once, when the function object is created** — which, inside a loop, means once per iteration with the current value. The value is stored on that function object (visible as `__defaults__`), and inside the body the parameter shadows the enclosing name, so the closure lookup never happens. Strengths: shortest to write, no extra names, obvious once you know the idiom. Weaknesses: the parameter is part of the public signature. `inspect.signature` shows `(v=0)`, and any caller can pass a different value — accidentally or through a framework that inspects the signature and decides to fill it. It is also unusable when the callable must match a fixed signature that already uses that position. Treat it as fine for local, throwaway lambdas and questionable for a callable you hand to someone else. ### 2. `functools.partial` ```python from functools import partial def scale(v): return v * 2 handlers = [partial(scale, v) for v in range(3)] print([h() for h in handlers]) # [0, 2, 4] ``` `partial` records the argument when the partial object is created and supplies it when the object is called. Two properties matter here: the bound value is captured eagerly, so the loop variable is irrelevant afterwards; and the real work lives in a normal named function that can be imported, tested and profiled on its own. The bound arguments stay introspectable on the partial object, which helps when you are debugging a registry of hundreds of callables. ### 3. A factory function ```python def make_handler(v): def handler(): return v * 2 return handler handlers = [make_handler(v) for v in range(3)] ``` The outer call creates a new local `v` per iteration, and the inner function closes over *that* — a different variable each time, so late binding is harmless. This is the most explicit form and the one to reach for when the inner function is more than one expression, needs a docstring, or needs a name in tracebacks. ### What does not fix it * **Using `def` instead of `lambda`.** They build the same kind of function object with the same closure rules. A nested `def` in a loop body captures the loop variable exactly as a lambda does. Syntax is not the issue. * **Copying the value to another name in the loop.** `w = v` puts another variable in the same scope; it also ends at the last value. * **Copying the list or the container being iterated.** The container was never the problem. * **Calling the functions "sooner".** That hides the bug rather than fixing it, and it stops being true the moment the callables are deferred again. ### Choosing between them For a quick local list of lambdas, the default parameter is idiomatic and everybody recognizes it. For anything registered, stored, or handed to a framework, prefer `functools.partial` or a factory: the value is bound without lying about the signature, the underlying function is testable on its own, and a reviewer can see the binding in one line. In a codebase where this bug has already happened once, the durable fix is structural — give the registration function a parameter for the per-iteration value, so the loop body has nothing to close over and the mistake becomes impossible to write. ### One more nuance `nonlocal` and `global` do not enter into this. They change *where a name is rebound*, not *when it is read*; a closure over a `nonlocal` variable is still late-binding. The fix is always a new binding per iteration, and in Python that means a call with a parameter.

  • Does using a nested def instead of a lambda avoid late binding?
    No. `def` and `lambda` build the same kind of function object with the same closure rules, so a nested `def` inside a loop body captures the loop variable exactly as a lambda does. What fixes it is passing the value in as a parameter — through a default, through `functools.partial`, or through a factory that takes it — not the syntax used to spell the function.
  • Can you fix it by copying the loop variable to another name inside the loop?
    No. `w = v` creates another variable in the same scope, and the closure will read that one at call time too, after it has finished at the last value. Only a binding created fresh per iteration helps, and in Python a fresh binding comes from a call — which is why every real fix ends up making the value a parameter.
  • When would you avoid the default-parameter trick?
    Whenever the callable is part of an interface. The default shows up in `inspect.signature`, so a dispatcher or framework that fills arguments by inspection can overwrite it, and it is impossible when the callback must match a fixed signature. For registered callbacks prefer `functools.partial` or a factory, which bind the value without changing the visible signature.

saying these in an interview costs you the question

  • Says replacing the lambda with a def fixes the capture
  • Copies the loop variable to a new name in the loop
  • Copies the container being iterated and expects a fix
  • Thinks functools.partial calls the function immediately
  • Believes default parameters are evaluated on every call
  • Reaches for global or nonlocal to bind the value

context