Why does [[0]*3]*3 break?

Multiplying a list repeats the reference, not the object. [[0]*3]*3 builds one inner list and points at it three times, so writing to one row writes to all of them. Use a comprehension — [[0]*3 for _ in range(3)] — which evaluates the inner expression once per row.

Overview

The bug

1Python
Output

One assignment changed all three rows. The reason is that * 3 on a list copies references, not objects — so grid holds three pointers to the same inner list.

2Python
Output

The inner [0] * 3 is fine, because integers are immutable — there is nothing to share dangerously. The outer * 3 is the problem, because a list is mutable and now has three names.

Lists & arraysConceptualMedium

Step through it

What to watch

  • All three rows share one address in the broken version.
  • One write appears in three places — nothing was copied.
  • The comprehension produces three distinct objects.

Say this out loud

"List multiplication copies references. There's one inner list and three pointers to it. A comprehension builds a fresh row each time, which is what you want."

Why does [[0]*3]*3 break?

What does [[0] * 3] * 3 create, and why does writing to one row change all of them?

Run it

The broken grid and the correct one side by side with their row identities printed, then the three levels of copying, then the mutable-default-argument bug that has the same cause.

3Python
Output
4Python
Output

The fix

5Python
Output

The comprehension evaluates [0] * 3 on every iteration, so each row is a distinct object.

ExpressionRows areSafe to mutate
[[0] * 3] * 3The same objectNo
[[0] * 3 for _ in range(3)]DistinctYes
[[0, 0, 0] for _ in range(3)]DistinctYes
[list(row) for row in template]Distinct copiesYes
copy.deepcopy(template)Fully independentYes
[[0] * 3] * 3 of immutables onlyShared, unobservablyRead-only

The last row is why the bug is subtle: sharing immutable objects is harmless and invisible. [0] * 1000 shares one integer object a thousand times and nothing can go wrong, because integers cannot be modified in place. The trouble begins only when the repeated element is mutable.

The same mistake, other costumes

Mutable default arguments — the most consequential version, because the sharing spans calls:

6Python
Output
7Python
Output

Class attributes — sharing across instances:

8Python
Output
9Python
Output

dict.fromkeys with a mutable default:

10Python
Output

Shallow copies of nested structures:

11Python
Output

All four are the same rule: Python assigns and copies references. A copy of a container duplicates the pointers, not the objects behind them.

What multiplication actually does

[x] * 3 builds a list holding the same reference three times. For immutable contents that is invisible — [0] * 3 is fine, because you can never mutate a 0. For a mutable inner object it is a trap, because all three names lead to one object.

The tell is that the bug only appears on write. Building and printing the grid looks perfect; the first assignment to a cell is when it falls apart.

The same idea, three ways

This is one instance of a general rule: assignment and shallow copies duplicate references, never the objects behind them.

b = a — another name for the same list. b = a[:] or list(a) or copy.copy(a) — a new outer list holding the same inner references, so nested contents are still shared. copy.deepcopy(a) — new objects all the way down, at the cost of walking the whole structure.

How to spot it in review

Any * n where the repeated element is a list, a dict, a set or a custom object is suspicious. The same rule explains the mutable default argument bug: def f(acc=[]) creates the list once, when the function is defined, and every call that omits the argument shares it.

The fix in both cases is the same — build the mutable thing at the moment it is needed rather than once, up front.

Copy semantics, precisely

OperationCopiesNested objects
b = aNothing — a second nameShared
a[:], list(a), copy.copy(a)The outer containerShared
copy.deepcopy(a)Everything, recursivelyIndependent
[f(x) for x in a]Per-element resultDepends on f

deepcopy is the correct tool when independence is required at every level, and it is not free: it walks the whole structure, tracks already-copied objects to handle cycles, and can be surprisingly slow on large graphs. For a list of lists, [list(row) for row in grid] is faster and clearer.

For grids specifically, NumPy sidesteps the question:

import numpy as np
grid = np.zeros((3, 3), dtype=int)
grid[0, 0] = 1                        # no aliasing: one contiguous buffer

Note that NumPy has its own version of the distinction — slices are views, not copies:

a = np.arange(6)
b = a[2:4]
b[0] = 99
a            # array([ 0,  1, 99,  3,  4,  5])  -- a changed too
c = a[2:4].copy()                      # explicit copy when needed

The lesson generalises: whenever a library offers cheap slicing, ask whether you received a view or a copy.

How to detect it

is and id answer the question directly:

12Python
Output

Mutate one element and look — the fastest check during debugging. Set grid[0][0] = 1 and print the whole structure.

Watch for * on a list of mutables, and for any default argument or class attribute that is a list, dict or set. Linters flag mutable defaults (B006 in flake8-bugbear, W0102 in pylint), and enabling that rule catches the most damaging variant automatically.

Questions people ask

Why does [0] * 3 work fine? Integers are immutable, so sharing them is unobservable.

Is * ever right for nested lists? Only if you never mutate the inner lists — and then a tuple of tuples states the intent better.

Why does the comprehension fix it? The expression is re-evaluated per iteration, creating a new object each time.

Does this affect tuples? ((0,) * 3,) * 3 is safe, because nothing can be mutated. A tuple containing a list has the same problem.

When do I need deepcopy? When nesting is arbitrarily deep or unknown. For a known two-level structure, a comprehension is faster.

Is list(a) a deep copy? No. Shallow — inner objects stay shared.

Why is a mutable default evaluated once? Default arguments are evaluated at function definition, and stored on the function object.

Recap in one screen

  • [[0] * 3] * 3 creates three references to one inner list, so mutating one row changes all of them.
  • Use [[0] * 3 for _ in range(3)] — the comprehension builds a fresh object per row.
  • The same rule causes mutable default arguments, shared class attributes, and dict.fromkeys with a list.
  • Slicing and list() are shallow copies; deepcopy is the recursive one, and it is slow.
  • Test for aliasing with grid[0] is grid[1], and let a linter catch mutable defaults for you.

How the code works

The broken grid and the correct one side by side with their row identities printed, then the three levels of copying, then the mutable-default-argument bug that has the same cause.

How the code works

  1. [hex(id(row)) for row in wrong]Three identical addresses. That single line is the whole explanation, and it is worth printing in an interview rather than describing.
  2. [[0] * 3 for _ in range(3)]The comprehension evaluates [0] * 3 once per iteration, so each row is a separate object. [0] * 3 itself is safe because integers cannot be mutated.
  3. shallow = original[:]A new outer list holding the same inner references. Appending to original does not affect it; mutating a nested list does. Both behaviours are shown.
  4. def broken(item, accumulated=[])The default is evaluated once, when the function is defined, so every call that omits the argument shares one list. Same cause: a mutable object created once and referenced many times.

Change one thing

  • Build the grid with [[0] * 3] * 3 and only ever read from it. It looks completely correct — the bug needs a write.
  • Replace deepcopy with copy.copy and re-run. The nested mutation reappears, which is the difference between the two in one line.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. [[0] * 3] * 3 creates:

  2. a[:] on a list of lists gives you:

  3. def f(x, acc=[]) misbehaves because the default is evaluated:

Cheat sheet

Why does [[0]*3]*3 break?

Multiplying a list repeats the reference, not the object. [[0]*3]*3 builds one inner list and points at it three times, so writing to one row writes to all of them. Use a comprehension — [[0]*3 for _ in range(3)] — which evaluates the inner expression once per row.

INTERVIEW · vizlearn.in/interview/the-nested-list-multiplication-bug.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.