skip to content

For g = [[]] * 3, why does g[0] += [1] change every row but g[0] = g[0] + [1] not?

level: middleimportance: nice to knowfreq 18%

answer

  1. One object sits in all three slots
  2. Mutation versus rebinding, not addition
  3. Augmented assignment extends in place
  4. Plain plus allocates a new list
  5. list.__iadd__ returns the same object

basics

~10 s

Augmented assignment on a list mutates it in place, and all three slots reference one list, so every row shows the change. The plus form builds a new list and rebinds only slot 0.

solid answer

~40 s

`g = [[]] * 3` puts one list object in all three slots. `g[0] += [1]` evaluates as an in-place extend on that object — `list` implements `__iadd__`, which mutates and returns the same object — so the append is visible through every slot, and the result is `[[1], [1], [1]]`. The rebinding that follows stores the same object back into slot 0, changing nothing. `g[0] = g[0] + [1]` instead calls `__add__`, which allocates a **new** list containing the concatenation, and assignment points slot 0 at that new object; slots 1 and 2 still reference the untouched original, giving `[[1], [], []]`. The lesson is that `+=` and `+` are different operations on lists, and the difference is only observable when the object is shared.

code

python · 10 lines
python
g = [[]] * 3
g[0] += [1]
assert g == [[1], [1], [1]]
assert g[0] is g[2]

h = [[]] * 3
h[0] = h[0] + [1]
assert h == [[1], [], []]
assert h[0] is not h[2]
assert h[1] is h[2]

go deeper

for a junior

Recall that += on a list changes the existing object while x = x + y makes a new one, and that the difference only shows when something else refers to the same list.

for a middle

Explain the two-step model of augmented assignment on a subscript: fetch the object, apply the in-place operation, store the result back. Then predict both printed results correctly.

for a senior

Be ready to argue the tradeoff rather than pick a winner: in-place growth avoids repeated copying and is linear, while building a new list protects other holders of the original reference.

for a principal

Own the guidance angle: a rule that bans one operator misses the point, so the standard you set should be about who else holds a reference to the object being grown.

### Two operators that look interchangeable On integers, `x += 1` and `x = x + 1` are indistinguishable, because integers are immutable and both forms must produce a new object and rebind the name. On lists they are genuinely different operations, and the difference becomes visible the moment the list is referenced from more than one place — which is exactly what sequence repetition arranges. Start from the setup. `g = [[]] * 3` evaluates the inner empty-list display once and fills three slots with references to that one object, so `g[0] is g[1] is g[2]` is `True`. ### What `g[0] += [1]` does Augmented assignment on a subscript is not a single atomic step. The interpreter fetches the object in slot 0, applies the in-place operation to it, and then stores the result back into slot 0. For a list, the in-place operation is `list.__iadd__`, which extends the list with the items of the right operand and returns **the same object**. So: 1. Fetch the shared list. 2. Mutate it: it now contains `[1]`. 3. Store the returned object — the same one — back into slot 0. Step 2 is what everyone sees, because the mutated object is the object every slot references. Step 3 is a no-op in effect. The result is `[[1], [1], [1]]`. ```pycon >>> g = [[]] * 3 >>> g[0] += [1] >>> g [[1], [1], [1]] >>> g[0] is g[2] True ``` ### What `g[0] = g[0] + [1]` does Here the right-hand side is evaluated first, as an ordinary expression. `list.__add__` does not touch its operands; it allocates a new list holding the concatenation. Assignment then rebinds slot 0 of the outer list to that new object. The original shared list is never mutated, so slots 1 and 2 still show an empty list, and slot 0 no longer shares with them: ```pycon >>> g = [[]] * 3 >>> g[0] = g[0] + [1] >>> g [[1], [], []] >>> g[0] is g[2] False ``` ### Why this matters beyond the puzzle The pair is a precise diagnostic for whether someone understands mutation versus rebinding. `+=` on a mutable object is mutation, and mutation is visible through every reference to that object. Plain `+` followed by assignment is construction plus rebinding, and rebinding is visible only through the name or slot you assigned. Repetition simply guarantees there are other references to be surprised by. It also explains a partial fix that looks like progress. Someone who discovers an aliased grid and "fixes" a row with `g[0] = g[0] + [...]` really does detach row 0 from the others — so the specific symptom they were chasing disappears — while rows 1 and 2 remain a single shared object. The bug is now smaller, rarer and harder to find. Rebuilding every row with a comprehension is the only fix that removes the sharing entirely. ### Performance is the other half of the tradeoff Because `+=` extends in place, it does not copy the existing contents; `list = list + other` allocates and copies the whole left operand every time. In a loop that repeatedly appends to a growing list, the `+` form is quadratic in the total size while the in-place form is linear. So neither operator is simply "the safe one": `+=` is the efficient choice for a list you own exclusively, and `+` is the choice when you must not disturb a list that other references can see. Knowing which situation you are in is the actual skill, and sequence repetition is a reliable way to end up in the second situation without realising it. ### Telling them apart without printing anything The reliable check is identity before and after. Record `id(g[0])`, run the operation, and look again: the in-place form leaves the id unchanged, because the object was mutated rather than replaced, while the concatenating form produces a different id for slot 0. That is a better habit than reading the printed grid, because the printed grid only reveals the difference when the shared object has already been written to, whereas the id tells you which operation you actually performed. The same check distinguishes `extend`-style growth from concatenation anywhere else in a codebase, and it is the fastest way to answer "did this line mutate something other code can see, or build something new?" during a debugging session. ### A related consequence The same in-place-then-store mechanism is why augmented assignment can succeed at mutating and then fail at storing: when the container refuses the store, the mutation has already happened. That interaction belongs with immutable containers, but it is the same two-step model — mutate the object, then store it back — that explains the behaviour here.

  • Does g[0] = g[0] + [1] fully repair an aliased grid?
    No. It detaches only slot 0; the remaining slots still reference one shared list, so a later in-place write through slot 1 still shows up in slot 2. It removes the symptom you were chasing while leaving a smaller, rarer version of the same bug. Rebuilding every row with a comprehension is the complete fix.
  • If += is the risky one here, should you prefer + for lists generally?
    No — they solve different problems. `+=` extends in place without copying, so repeatedly growing a list with it is linear, while `x = x + other` in a loop copies the whole list each time and is quadratic. Use in-place growth for a list you own exclusively, and build a new list when other references must not observe the change.

saying these in an interview costs you the question

  • Says += and + are equivalent for lists
  • Thinks += rebinds without mutating the object
  • Claims + mutates the left operand in place
  • Believes the difference exists for integers too
  • Treats the plus form as a complete fix for aliasing

context