skip to content

Why does [[0] * 3] * 3 build a grid where setting one cell changes every row?

level: juniorimportance: must knowfreq 70%

answer

  1. Repetition copies pointers, not objects
  2. Count how many lists actually exist
  3. Inner expression is evaluated once
  4. grid[0] is grid[1] answers it
  5. Comprehension re-evaluates per iteration

basics

~10 s

Multiplying a list repeats references, not objects. The inner list is built once and its reference stored three times, so the three rows are one object: grid[0][0] = 1 appears in all of them.

solid answer

~40 s

`[[0] * 3] * 3` is two separate steps. The inner display `[0] * 3` is evaluated **once**, producing a single list object; the outer `* 3` then builds a new three-slot list in which every slot holds a reference to that same row. So the grid has exactly two list objects, not four, and `grid[0] is grid[1]` is `True`. Writing `grid[0][0] = 1` mutates the one shared row, and printing the outer list shows that same row three times. The inner `[0] * 3` is harmless because integers are immutable — you can never mutate `0` in place. The fix is a comprehension, `[[0] * 3 for _ in range(3)]`, which re-evaluates the inner display on every iteration and therefore creates three distinct row objects.

code

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

go deeper

for a junior

Be ready to state the rule out loud: multiplying a list repeats references, so the inner list exists once. Recall the fix, a comprehension, and be able to predict the printed output of the three-row grid after one cell write.

for a middle

Explain the mechanics: the inner display is evaluated once before repetition, the outer list holds three references to one object, and mutation versus rebinding behave differently. Show how identity, not equality, proves it.

for a senior

An interviewer expects you to spot this pattern in a diff and know how it fails in production — silently, as duplicated state, long after the line ran. Be ready to say why copying the outer list is not a fix.

for a principal

Own the guidance angle: the general rule for when repetition is safe, why the failure is invisible to equality-based tests, and how to make it a review or lint-level check rather than a lesson each engineer learns by losing a day.

### What the `*` operator does to a list Sequence repetition on a list produces a **new list whose slots are filled with the same references** as the original. It never inspects, copies or recreates the elements — it copies pointers. That single sentence explains the whole puzzle, but the puzzle is only visible when the repeated element is mutable. Work through `[[0] * 3] * 3` as the interpreter does, inside out: 1. `[0] * 3` is evaluated **once**, yielding one list object — call it `row` — holding three references to the integer `0`. 2. `[row]` builds a one-element outer list holding one reference to `row`. 3. `* 3` builds a **new** three-slot list, and each of the three slots is filled with the same reference to `row`. So the expression creates exactly two list objects: one row, and one container that points at it three times. The row's reference count rises to three; no second or third row ever exists. ### Proving it rather than believing it Identity is the tool, because equality cannot see the difference — `[[0] * 3] * 3 == [[0] * 3 for _ in range(3)]` is `True`. Both compare equal element by element; only identity reveals that one of them is three views of a single object. ```pycon >>> grid = [[0] * 3] * 3 >>> grid[0] is grid[1] is grid[2] True >>> len({id(row) for row in grid}) 1 >>> good = [[0] * 3 for _ in range(3)] >>> len({id(row) for row in good}) 3 ``` A set of `id()` values collapsing to one is the cleanest demonstration: three slots, one object. (Comparing `id()` values is only meaningful while every object is still alive, which it is here because the grid holds them.) ### Why the write propagates `grid[0][0] = 1` is two operations: fetch the object in slot 0 of the outer list, then **mutate** that object's slot 0. The mutation lands on the one row that every outer slot references, so reading `grid[1][0]` afterwards reads the same memory and sees `1`. Nothing was copied and nothing was corrupted; the display simply prints one object three times. Contrast that with `grid[0] = [9, 9, 9]`, which does not mutate anything — it **rebinds** slot 0 of the outer list to a brand-new list. Afterwards row 0 is independent while rows 1 and 2 still share the original. That half-fixed state is a classic follow-up trap: the candidate patches one row, sees the symptom move, and concludes the grid is fine. ### Why the inner `[0] * 3` is safe The same operator, the same reference-copying behaviour — but integers are immutable. There is no operation that changes the object `0` into something else, so sharing it three times is unobservable. `row[0] = 1` rebinds a slot rather than mutating an integer, and touches only that row. The rule generalises: **repetition is safe exactly when the repeated element is immutable, or when you never mutate it.** `[None] * n` and `[0] * n` for pre-allocation are idiomatic and correct; `[[]] * n`, `[{}] * n` and `[SomeObject()] * n` are the trap, because the constructor or literal on the left runs once. ### The correct construction ```python grid = [[0] * 3 for _ in range(3)] ``` The difference is *when* the inner expression is evaluated. In the multiplication form it is evaluated once, before repetition; in the comprehension it is re-evaluated on every iteration of the loop, so each iteration produces a fresh object. For a variable-sized grid the same shape scales: `[[0] * cols for _ in range(rows)]`. If you already hold a list of rows and want independent ones, rebuild each row rather than copying the outer list — copying the outer list only duplicates the pointer array, and the duplicated pointers still address the same rows. ### How this shows up in review The smell to look for is `* n` applied to a list whose element is a mutable literal, a call, or a name bound to a mutable object. In a code review that pattern deserves a question every time: either the elements are immutable and it is fine, or the author wanted independent objects and has written a bug that will surface much later, as a mysterious "every record changed" report rather than as an exception.

  • Does the same trap apply to the inner [0] * 3, and why not?
    It repeats references too, but the repeated element is the integer 0, which is immutable — there is no operation that changes it in place, so the sharing is unobservable. Assigning `row[0] = 1` rebinds that slot rather than mutating the integer. Repetition is only dangerous when the repeated element is mutable and something mutates it.
  • If you patch it with grid[0] = [0, 0, 0], is the grid fixed?
    No. That rebinds only slot 0 of the outer list to a new object; slots 1 and 2 still hold the original shared row, so writing `grid[1][0]` still shows up in `grid[2]`. It is a half-fix that moves the symptom instead of removing it. Rebuild every row — `[[0] * 3 for _ in range(3)]` — or replace each slot individually.
  • Can == tell you whether the rows are shared?
    No. `[[0] * 3] * 3 == [[0] * 3 for _ in range(3)]` is `True`, because equality compares values element by element and both hold the same numbers. Only identity distinguishes them: `grid[0] is grid[1]`, or collecting `id()` values for the rows and seeing how many distinct ones there are.

It is like printing one page and hanging three mirrors around it: you appear to have three pages, but writing on the page changes what all three mirrors show.

saying these in an interview costs you the question

  • Says list repetition copies or clones the inner list
  • Blames integer caching or interning for the shared rows
  • Claims == would reveal that rows are shared
  • Rebinds one row and calls the whole grid independent
  • Thinks [0] * 3 is equally dangerous as [[0]] * 3
  • Describes it as a CPython bug rather than reference semantics

context