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.
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
- What is a closure, and how does one end up leaking memory?mediumAlso on closures4 min
- The business says "we need a dashboard" and gives you a three-week deadline. Walk me through what you do first.mediumAlso on scoping5 min
- The sponsor has already bought the software and wants you to write the requirements for it. How do you approach that?hardAlso on scoping4 min
- How do you scope a proof of concept so that it closes the deal?hardAlso on scoping7 min
- How do you spend the first ten minutes of a system design round?mediumAlso on scoping5 min
- Adding a second LEFT JOIN doubled the revenue figure on a report. Explain what happened and write the correct query.mediumSame kind of round: coding4 min
- Your test still reaches the network even though you patched the client. Talk me through fixture scope, parametrisation, and where a patch has to point.mediumSame kind of round: concept4 min
- This form marks invalid fields with red text and a red border. What has to change?mediumSame kind of round: concept4 min