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

Python

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.

Python

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

one list object
row : [0, 0, 0] @0x1f40
One row object exists.
objects1
Step 1 of 4

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.

The fix

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:

Class attributes — sharing across instances:

dict.fromkeys with a mutable default:

Shallow copies of nested structures:

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:

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.

Check yourself

0 of …

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 · AI.Cruz.in/interview/the-nested-list-multiplication-bug.html

About the author

AI.Cruz builds and maintains AI.Cruz. 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.