skip to content

Repetition and Shared References

[[0]*3]*3 builds three names for one row, because `*` repeats references instead of copying objects. Interviewers use the grid-initialization bug to check that you see the aliasing.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

3

Why does `[[0]*3]*3` change a whole column when you assign one cell?

level: juniorimportance: must knowfreq 70%

answer

  1. Ask what the outer list stores
  2. Repetition never copies objects
  3. Three slots, one row object
  4. g[0] is g[1] is True
  5. Rebuild the row inside a comprehension

basics

~10 s

* repeats references, not objects. [[0]*3]*3 stores one inner list three times, so mutating g[0][0] is visible through every row. Build the rows separately with [[0]*3 for _ in range(3)].

solid answer

~40 s

Python evaluates the inner `[0]*3` once, producing a single list object. The outer `*3` then builds a new outer list whose three slots hold the *same* reference to that one row: repetition copies references, never the objects behind them. So `g[0][0] = 9` mutates the one shared row, and `g[1][0]` and `g[2][0]` read back `9` as well; `g[0] is g[1]` is `True`. The fix is a comprehension, `[[0]*3 for _ in range(3)]`, because the row expression is re-evaluated on each iteration and produces a fresh list every time. Repetition itself is not broken — the inner `[0]*3` is perfectly safe, since integers are immutable and `xs[0] = 9` rebinds a slot rather than mutating a shared object.

code

pycon · 8 lines
pycon
>>> g = [[0] * 3] * 3
>>> g[0][0] = 9
>>> g
[[9, 0, 0], [9, 0, 0], [9, 0, 0]]
>>> g[0] is g[1]
True
>>> len({id(row) for row in g})
1

go deeper

for a junior

Be ready to predict what [[0]*3]*3 prints after one cell assignment, and to write the comprehension fix from memory. Interviewers use this as a fast check that you distinguish names from the objects they reference.

for a middle

Explain the mechanism: the inner expression is evaluated once, the outer repetition stores that single reference three times, and mutation through any index is visible through all. Demonstrate the is check rather than an equality check.

for a senior

Show where this reaches production — grids, per-key buckets, per-shard accumulators built by repetition — where the symptom is a duplicated or misattributed write rather than an exception, and say how you would catch it in review.

for a principal

Own the guidance: 'n independent mutable containers' is a shape that needs a per-item factory, not a repeated instance. Decide whether a lint rule, a shared construction helper or a review convention is the cheaper control for your codebase.

### The two repetitions `[[0]*3]*3` contains two separate repetitions, and they behave differently only because of what each one repeats. The inner `[0]*3` runs first. It repeats the integer `0`, producing one new list of three slots, each holding a reference to the same `0` object. That is harmless, and we come back to why. The outer `*3` then repeats *that one list object*. Sequence repetition builds a new list of length three and fills its slots with the reference already sitting in the operand's slot — a reference to the single row. It calls no copy machinery, it does not consult the element's type, and it never allocates a second row. You end up with one outer list, one inner list, and three references pointing at that inner list. ### Why the mutation looks like magic `g[0][0] = 9` is read left to right: fetch `g[0]` (the shared row), then store `9` into slot 0 of that row. Exactly one object is mutated. But `g[1]` and `g[2]` are the *same* object, so reading `g[1][0]` afterwards yields `9` too. Nothing was copied, so nothing needs to be kept in sync — the three rows were never three rows. The confusion is almost always a mental model in which a list *contains* its elements. In CPython a list is a vector of pointers; it contains references. Repetition duplicates pointers, and duplicating a pointer to a mutable object is precisely what aliasing means. ### Proving it Equality cannot tell you: three distinct all-zero rows compare equal to three references to one row. Identity can. `g[0] is g[1]` is `True` in the broken version and `False` in the fixed one, and `len({id(row) for row in g})` collapses to `1` when the rows are shared. Reach for identity whenever the symptom is 'a write showed up somewhere I did not write'. ### The fix, and why it is a fix `[[0]*3 for _ in range(3)]`. A comprehension evaluates its element expression once per iteration, so `[0]*3` runs three times and yields three independent lists. The explicit loop with `rows.append([0]*3)` works for the same reason. What matters is that a fresh object is *constructed* per row, not that a cleverer operator was used. The cost difference is real and is the whole point: the broken version allocates one row, the correct one allocates n. A grid of n independent rows genuinely costs n rows' worth of slots. If you truly want n references to one row, say so with an immutable element — `((0, 0, 0),)*3` — where the sharing cannot hurt you. ### The same trap in other clothes * `([],)*3` — a tuple is immutable, but the list inside it is not; you get three references to one tuple whose single element is one shared list. * `[[[0]]*2]*2` — repetition at every level, sharing at every level. * `rows = base_rows * 2` — the outer list is new, but every existing row is now referenced twice inside it. * `+` is equally shallow: `rows + rows` builds a new outer list in which each row object appears twice. The pattern to recognise is not the `*` operator. It is 'n slots, one mutable object'. ### When repetition is exactly the right tool `[0]*n`, `[None]*n` and `['-']*n` are idiomatic, fast preallocation. Because ints, `None` and `str` cannot be mutated, no code can tell 'n references to one object' apart from 'n copies'. Assigning `xs[i] = 9` rebinds slot i and leaves the other slots untouched, because an index assignment changes the *list*, not the object that slot used to reference. That is why the inner `[0]*3` was never the bug — and why blaming repetition in general is the wrong lesson to take away. ### Why interviewers keep asking it One line of code separates two mental models. A candidate who says 'the rows are copies and `*` is broken' holds the container-holds-values model. A candidate who says 'the outer list holds one row three times, check `g[0] is g[1]`, build the rows in a comprehension' holds the names-and-objects model — and that model is what makes the rest of the language, from argument passing to closures to mutable class attributes, predictable instead of surprising.

  • Does `[[0]*3] + [[0]*3]` suffer from the same aliasing?
    No. Each `[0]*3` is evaluated separately, so the two rows are distinct objects and `+` builds a new outer list holding both. `+` is still shallow, though: `rows + rows` copies only references, so every existing row would appear in the result twice. The trap is repeating one object, not the operator you used.
  • How would you prove the aliasing without mutating anything?
    Compare identity, not value: `g[0] is g[1]` is `True`, or `len({id(row) for row in g}) == 1`. Equality cannot help, because three distinct all-zero rows compare equal to three references to one row. Whenever the symptom is a write appearing where you did not write, identity is the discriminating check.
  • Is the comprehension version more expensive than the repetition version?
    Slightly, and correctly so. It evaluates the row expression once per iteration and allocates n rows instead of one, so memory and construction work scale with rows times columns. That is what an n-by-m grid of independent rows costs; the repetition version is cheap only because it is not really a grid.

Repetition hands three people the same clipboard rather than three copies of the form: whatever any one of them writes, all three read back.

saying these in an interview costs you the question

  • Says the three rows are copies of one another
  • Uses `==` to test whether two rows are the same object
  • Believes `*` shallow-copies each element it repeats
  • Thinks the bug lives in the inner `[0]*3`
  • Claims a list of plain integers is affected the same way
  • Fixes it by writing the same repetition on three lines

context

open as a page

Why is `[0]*n` safe in Python while `[[]]*n` shares one list?

level: middleimportance: should knowfreq 55%

basics

~20 s

Repetition always copies references, never objects. Nothing can mutate an integer, so sharing 0 is unobservable and xs[0] = 9 merely rebinds a slot. With [[]]*n every slot references one list, so xs[0].append(1) shows up everywhere.

open as a page

A feature-flag service fires one registered callback for every flag; how do you trace the duplicated side effect to list repetition?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Reproduce past the evaluation cache, then compare identity rather than value: len({id(b) for b in buckets}) == 1 shows the per-flag buckets are one list built by [[]] * n. Rebuild them per flag with a comprehension.

open as a page