skip to content

Lists and Tuples

The sequence types you use everywhere, seen from the inside: how a list grows, what each method costs, where a tuple fits better. The wrong operation in a loop is Python's classic performance bug.

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

questions

17

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

open as a page

When should you choose a tuple over a list in Python, and why?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Use a tuple for a fixed-shape record whose positions each mean something, and a list for a variable-length collection of like items. Tuples are also hashable, so a tuple can key a dict or join a set; a list cannot.

open as a page

Does putting a list inside a Python tuple make that list immutable?

level: juniorimportance: must knowfreq 62%

basics

~20 s

No. A tuple freezes only its own slots, meaning which objects they point at. A list stored in a tuple is still an ordinary mutable list, so appending to it works; only rebinding the slot fails.

open as a page

What is the difference between list.append() and list.extend() in Python?

level: juniorimportance: must knowfreq 76%

basics

~10 s

list.append(x) adds x to the end as one single element, so appending a list nests it. list.extend(iterable) walks the argument and adds each of its items separately, growing the list by that many elements.

open as a page

Why is Python's list.append amortized O(1) but list.insert(0, x) O(n)?

level: middleimportance: must knowfreq 65%

basics

~20 s

append usually writes into a spare slot the list already reserved, and the occasional resize copies pointers into a proportionally larger array, so the cost averages to constant. insert(0, x) shifts every existing pointer right, on every call.

open as a page

Why is `x in my_list` O(n), and how do you fix a loop that tests it repeatedly?

level: middleimportance: must knowfreq 58%

basics

~20 s

A list keeps no index of its contents, so in walks it element by element — O(n). Running that test once per item of another collection is O(n²); build a set or dict once, before the loop, for O(1) average lookups.

open as a page

What is the difference between len(a_list) and sys.getsizeof(a_list) in Python?

level: juniorimportance: should knowfreq 35%

basics

~20 s

len returns how many elements a list currently holds. sys.getsizeof returns the bytes the list object itself occupies: its header plus its internal array of pointer slots, including spare unused ones. It never counts the objects the list points at.

open as a page

When is repeating a list with * n safe, and when does it silently alias?

level: middleimportance: should knowfreq 45%

basics

~20 s

Repetition is safe when the repeated element is immutable or never mutated — [0] * n, [None] * n, tuples, strings. It aliases when the element is mutable, because every slot holds one object built once.

open as a page

What does returning a tuple instead of a list from a Python function promise its callers?

level: middleimportance: should knowfreq 42%

basics

~20 s

It promises a fixed-length snapshot the caller does not own. Tuples have no append, extend or item assignment, so a caller needing a change must build a new object rather than mutate the one handed back.

open as a page

Why does hash() raise TypeError on a Python tuple that contains a list?

level: middleimportance: should knowfreq 48%

basics

~20 s

A tuple's hash is computed by hashing each of its items and mixing the results, so a tuple is hashable only if every item is. Lists opt out of hashing, so the whole tuple becomes unhashable and cannot key a dict.

open as a page

How do list.copy(), lst[:] and copy.deepcopy() differ when copying a Python list?

level: middleimportance: should knowfreq 52%

basics

~20 s

list.copy() and lst[:] are identical shallow copies: a new list holding the same element references, O(n) in pointers. copy.deepcopy() recursively copies the elements as well, so nested objects are independent — correct where a shallow copy is not, but far more expensive.

open as a page

A nightly report generator built 6,800 row buffers by repeating one template list; how do you prove the rows are shared and fix it?

level: seniorimportance: should knowfreq 33%

basics

~10 s

Prove it with identity: collect id() for the rows and see one distinct value, or check rows[0] is rows[-1]. Fix it by constructing each row inside a comprehension instead of repeating one object.

open as a page

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

level: middleimportance: nice to knowfreq 18%

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.

open as a page

Why does a Python list of 10 million float readings use several times the raw data size?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

A list stores eight-byte pointers rather than the numbers, so ten million elements is about 80 MB of slots plus over-allocated slack. Each reading is also a separate float object of about 24 bytes, adding roughly 240 MB more.

open as a page

When does swapping per-record lists for tuples actually cut memory in a search-index rebuilder holding millions of records?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

It pays when records are numerous, small and fixed-shape. On CPython 3.14 a five-item list costs 104 bytes against 88 for the tuple, 120 if grown by appends — real at ten million records, noise at ten thousand.

open as a page

On a Python tuple t = ([1], 2), why does t[0] += [3] raise yet still extend the list?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Augmented assignment is two steps: the list extends itself in place, then Python tries to store the result back into the tuple slot. The store is what raises TypeError, and nothing undoes the extend that already happened.

open as a page

Why does `lst[:] = items` behave differently from `lst = items` for other holders of that list?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

lst = items rebinds one name and leaves the original list object untouched, so other references still see the old contents. lst[:] = items replaces that object's elements in place, so every holder of it observes the change.

open as a page