Skip to content
QSWEQB
mediumConceptCodingEntryMidSenior

Why do the functions you build inside a loop all end up returning the last value?

A closure captures the variable, not the value it held at the time. All three lambdas share one cell for the loop name, the loop leaves that cell holding its final value, and every call reads it then. Bind per iteration with a default argument or a factory to fix it.

4 min readUpdated 2026-07-26

What the interviewer is scoring

  • Does the candidate say the closure captures a variable rather than describing the values as being overwritten
  • Whether the fix is justified by when the binding happens, not offered as a memorised idiom
  • Can they explain why an assignment late in a function body causes UnboundLocalError earlier in it
  • That nonlocal and global are distinguished by which scope they rebind, with a case for each
  • Whether they know a class body is skipped by the enclosing-scope lookup, and what that breaks

Answer

A closure holds a variable, not a value

When a nested function refers to a name from an enclosing scope, Python does not copy the current value into the function. It creates a cell — a small mutable box — and both the enclosing scope and the nested function refer to that same box. The nested function reads the box when it is called, which may be long after the enclosing code has finished changing what is in it. You can see the machinery directly: a closure's captured cells are in __closure__, and each has a cell_contents attribute.

A loop creates exactly one binding for its loop name, rebinding it on each pass, so every function defined in the body closes over the same cell.

def build():
    makers = []
    for i in range(3):
        makers.append(lambda: i)      # every lambda closes over the same cell for i
    return makers

fns = build()
print([f() for f in fns])                     # [2, 2, 2] - all read the cell afterwards
print(fns[0].__closure__[0].cell_contents)    # 2

Nothing was overwritten in the sense of values being lost. Only one value ever existed at a time, and all three functions are looking at the same place.

Bind at definition time to get one per iteration

The two fixes both work by evaluating something once per iteration, at the moment the function is created, instead of deferring the read.

from functools import partial

by_default = [lambda i=i: i for i in range(3)]     # default evaluated per definition
print([f() for f in by_default])                   # [0, 1, 2]

def make(i):                                       # each call gets its own cell
    return lambda: i

by_factory = [make(i) for i in range(3)]
print([f() for f in by_factory])                   # [0, 1, 2]

by_partial = [partial(lambda i: i, i) for i in range(3)]   # argument bound now
print([f() for f in by_partial])                           # [0, 1, 2]

The default-argument version is the same mechanism that makes a mutable default argument dangerous, running in your favour: defaults are evaluated when the def or lambda executes, so each iteration snapshots the current i. Its cost is a parameter in the public signature that a caller can override, so prefer the factory or partial for anything that is not a throwaway.

This is not a puzzle-only problem. It is why a loop that wires up button callbacks, retry handlers, or Thread(target=...) closures over a loop variable produces N handlers that all act on the last item, and the bug survives testing whenever the loop happens to run once.

Assignment decides scope for the whole function

The other half of Python scoping that interviewers probe is the rule that a name assigned anywhere in a function body is local everywhere in that body. The compiler decides this statically, before any code runs, which is why the failure arrives earlier than the offending line.

count = 0

def broken():
    print(count)      # UnboundLocalError: the assignment below made count local
    count = 1

def works():
    global count      # rebind the module-level name
    count += 1

def outer():
    total = 0
    def inner():
        nonlocal total    # rebind outer's local, not a global
        total += 1
    inner(); inner()
    return total

print(works.__code__.co_names)     # ('count',) - looked up globally
print(outer())                     # 2

count += 1 counts as an assignment, which is why augmented assignment on a global or an enclosing local needs a declaration while merely reading it does not. global reaches all the way to module level and skips any intervening function; nonlocal binds to the nearest enclosing function scope and is a compile-time error if there is none. Neither is needed to mutate an object you can already see: cache[key] = value rebinds nothing, so it works without a declaration, and this asymmetry between rebinding a name and mutating an object is the part worth articulating.

The scope that is not in the chain

Lookup follows local, enclosing function, global, builtins. A class body is conspicuously not on that list. It executes as its own namespace, that namespace becomes the class's attributes, and no function defined inside it can see those names as free variables — which is why methods reach class state through self or the class name rather than directly.

The version of this that appears in real code involves comprehensions. A comprehension has its own scope, so it also skips the class body, with one carve-out: the iterable of the outermost for is evaluated in the enclosing scope before the comprehension's scope is entered.

class Config:
    limits = [1, 2, 3]
    factor = 10
    doubled = [x * 2 for x in limits]        # fine: limits is the outer iterable
    # scaled = [x * factor for x in limits]  # NameError: factor is not reachable

print(Config.doubled)                        # [2, 4, 6]

The fix is a default argument on a lambda, a module-level constant, or moving the computation into __init_subclass__ or a method. Since Python 3.12 and PEP 709 comprehensions are inlined into the enclosing function rather than creating a separate function object, which made them measurably faster and removed a frame from tracebacks, but the scoping was deliberately preserved: the loop variable still does not leak and the class-body restriction still applies.

Calling it a quirk loses the mark

The answer that fails the follow-up is "Python evaluates the lambda lazily" or "it is a known gotcha". Both suggest an inconsistency, and there is none — the rule is uniform and it is the same rule that makes a recursive function able to call itself, and that lets a decorator's wrapper read a variable the decorator assigns after the wrapper is defined. Late binding is a feature you rely on constantly. What you need is to notice when the variable you captured is going to keep changing, and then to bind deliberately.

Likely follow-ups

  • Why does a comprehension inside a class body raise NameError for a class attribute?
  • What is in a function's __closure__, and how would you read the captured value?
  • How would you fix the loop with functools.partial instead of a default argument?
  • Why does adding one assignment to a function change the meaning of every read of that name in it?

Related questions

Further reading

closuresscopinglate-bindingcomprehensionsnonlocal