Where does a := assignment inside a Python comprehension bind its target name?
answer
- One construct escapes the comprehension's scope
- Assignment expressions were designed to reach out
- Binds in the containing scope, not the comprehension
- Rebinding the loop variable is a SyntaxError
- Last value that reached the assignment survives
basics
~20 sIn the scope containing the comprehension, not the comprehension's own implicit scope. PEP 572 chose that deliberately so a comprehension can export a computed value; afterwards the name holds the value from the last item that reached the assignment.
solid answer
~40 sAn assignment expression is the one construct that binds *outward* from a comprehension. `[y for x in data if (y := f(x)) > 0]` puts `y` in the containing function, module or comprehension-free enclosing scope, honouring a `nonlocal` or `global` declaration there — PEP 572 chose that so the idiom of computing a value once and both filtering and using it actually works, and so a running value survives the loop. Two things are compile-time errors: rebinding the comprehension's own iteration variable, and using `:=` inside a comprehension in a class body. In a generator expression nothing is bound until the generator actually runs, so reading the name before consuming the generator raises `NameError` or `UnboundLocalError`, and after partial consumption it holds the value from the last item produced.
code
python · 10 linesdata = [1, -2, 3, -4]
kept = [y for x in data if (y := x * 10) > 0]
print(kept) # [10, 30]
print(y) # -40: every item reached the filter, so the last one wins
empty = [z for x in [] if (z := x) > 0]
try:
print(z)
except NameError:
print("nothing ever reached the assignment")go deeper
Recall that := inside a comprehension is legal and that the name it assigns is still readable after the comprehension has finished. Be able to say which value it holds: the one from the last item that reached the assignment.
Explain the mechanism: the for targets are local to the implicit scope, while an assignment expression is specified to bind in the containing scope. Name the two SyntaxErrors — rebinding the iteration variable, and using it in a class-body comprehension.
Show the timing judgement: in a generator expression the target is bound progressively, so a half-consumed generator leaves a half-updated name, and an unconsumed one binds nothing. Be ready to argue when the idiom removes a duplicate computation and when it is just cleverness.
Own the readability policy. Decide where your codebase draws the line between a walrus that removes a repeated expensive call and a comprehension that filters, accumulates and transforms at once — and encode that decision in review guidance rather than re-arguing it per pull request.
## The rule A comprehension's `for` targets are local to its implicit function scope. An assignment expression written inside that comprehension is the deliberate exception: PEP 572 specifies that `:=` binds in the *containing* scope. If the comprehension sits in a function, the target becomes a local of that function; at module level it becomes a global; and if the comprehension is itself nested inside another comprehension, the binding skips outward to the nearest scope that is not a comprehension. A `nonlocal` or `global` declaration in that containing scope is honoured, so the binding can be pushed further out still. ```python data = [3, -1, 4] kept = [y for x in data if (y := x * 10) > 0] print(kept) # [30, 40] print(y) # 40 — the last value the filter computed ``` ## Why it was designed that way The motivating use is computing a value once. Without `:=` you either call an expensive function twice — once in the filter and once in the output expression — or you build an intermediate comprehension to hold the results. With `:=` the filter computes it and the output expression reuses the same binding: ```python results = [y for x in data if (y := expensive(x)) is not None] ``` If `:=` had bound inside the comprehension's own scope, the name would still be usable within the comprehension, so the immediate use case would work — but PEP 572 went further and made it bind outward, because a comprehension is otherwise a one-way street: it can read the enclosing namespace but never write to it. The outward binding gives you a supported way to carry one value back out, which is exactly what makes patterns like "find and keep the last match" or "accumulate a running total while filtering" expressible. ## Two compile-time errors The design draws two hard lines, both reported as `SyntaxError` at compile time rather than at run time. First, an assignment expression may not rebind the comprehension's iteration variable. `[(i := 0) for i in range(3)]` is rejected, because the iteration variable is owned by the comprehension's scope while `:=` targets the outer one, and allowing both meanings for one name would be incoherent. The same applies to a name used as an iteration variable in *any* of the comprehension's `for` clauses. Second, an assignment expression may not appear in a comprehension that is written in a class body. The comprehension's implicit scope cannot see or write the class namespace at all, so there is no sensible scope for the binding to land in, and the language rejects the program rather than picking one. ## Timing, and why generator expressions differ For a list, set or dict comprehension the loop runs to completion immediately, so by the next statement the target holds the value from the last item that reached the assignment. Note that word *reached*: if the assignment sits inside a filter that appears after another filter, or in the output expression that a filter can skip, only the items that actually evaluated it contribute. If the comprehension produced nothing — an empty source, or every item filtered out before the assignment — the name is never bound. In a function that is an `UnboundLocalError` on read, because the compiler already marked the name as a local of that function; at module level it is a `NameError`. A generator expression is lazier and the timing follows. The generator's body does not run until you iterate it, so the target is bound progressively, one item at a time, and reading it after consuming only part of the generator gives you the value from the last item produced. A generator expression that is created and never consumed binds nothing at all. This makes `:=` inside a generator expression a genuinely stateful thing to hand around, and it is a good reason to keep such generator expressions short-lived and consumed close to where they are built. ## Style The idiom earns its place when it removes a duplicated computation or a throwaway intermediate list. It stops earning its place quickly: a comprehension that both filters on `:=`, mutates a running accumulator with `:=` and produces a transformed output is doing three things at once, and a plain `for` loop reads better. Interviewers who ask this are usually probing whether you know the binding escapes at all — candidates who assume the comprehension swallows it are the ones who later write code that reads the name afterwards and cannot explain why it sometimes raises.
- Why did PEP 572 make := bind outward rather than into the comprehension's own scope?A comprehension can read the surrounding namespace but never write to it, so there was no supported way to carry a computed value back out. Binding outward gives one, which makes patterns like keeping the last match or a running accumulator expressible. It also honours a `nonlocal` or `global` declaration in that containing scope, so the target can be pushed further out deliberately.
- What is bound if the := sits in a generator expression that is never consumed?Nothing. The generator's body does not execute until you iterate it, so the target is bound progressively as items are produced. Reading it before any consumption raises `UnboundLocalError` inside a function or `NameError` at module level, and after partial consumption it holds the value from the last item produced — which is why such generator expressions should be consumed close to where they are built.
- Which two uses of := inside a comprehension are SyntaxErrors?Rebinding any of the comprehension's own iteration variables, and using an assignment expression in a comprehension written in a class body. The first would give one name two owning scopes; the second has no scope to bind into, because the comprehension's implicit scope cannot reach the class namespace. Both are reported at compile time, not at run time.
saying these in an interview costs you the question
- Says the walrus target dies with the comprehension
- Claims := can rebind the comprehension's loop variable
- Thinks the name holds the first value, not the last
- Assumes the name is bound even when nothing was iterated
- Says a generator expression binds the target at creation time