skip to content

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

level: middleimportance: should knowfreq 45%

answer

  1. The operator copies references either way
  2. Ask whether the element is mutable
  3. Left operand is evaluated exactly once
  4. Immutable fill is idiomatic and fast
  5. Comprehension re-runs its expression per item

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.

solid answer

~50 s

The operator always does the same thing: it fills a new sequence with the **same references**. Whether that is a bug depends entirely on the element. If the element is immutable — an integer, `None`, a string, a tuple of immutables — sharing is unobservable, and `[0] * 10_000` or `[None] * n` for pre-allocation is idiomatic and fast. If the element is mutable, or is produced by a call, the expression on the left runs **once** and every slot points at that one object, so any in-place mutation is seen through all of them. The fix is a comprehension, which re-evaluates its expression per iteration: `[[] for _ in range(n)]`, `[make_buffer() for _ in range(n)]`. A useful review heuristic: if the left operand contains a mutable literal, a constructor call, or a name bound to a mutable object, repetition is almost certainly wrong.

code

python · 8 lines
python
safe = [None] * 4
safe[0] = "set"
assert safe == ["set", None, None, None]

aliased = [[]] * 4
aliased[0].append(1)
assert aliased == [[1], [1], [1], [1]]
assert len({id(x) for x in aliased}) == 1

go deeper

for a junior

Learn the short rule: numbers, strings, None and tuples are fine to repeat; lists, dicts, sets and objects are not. Recall the comprehension form as the replacement when the element is mutable.

for a middle

Be ready to explain why one rule produces both outcomes — the operator always copies references, and only mutability makes that observable. Show that a call on the left runs exactly once.

for a senior

An interviewer expects the review-time heuristic and the reason this survives tests: aliased and correct constructions compare equal until the first in-place mutation, which usually happens elsewhere in the code.

for a principal

Own the tradeoff: repetition for immutable fills is measurably faster and should not be banned outright, so the guidance you set has to be about the element's mutability rather than a blanket rule about the operator.

### One behaviour, two outcomes Sequence repetition has no special cases. `seq * n` builds a new sequence of `len(seq) * n` slots and fills them with the references already in `seq`, cycled. It does not copy elements, does not call any copy protocol, and does not care whether the elements are mutable. Everything that looks like two different behaviours comes from the elements, not the operator. **Safe case — immutable elements.** `[0] * 8`, `[None] * n`, `[""] * n`, `[(0, 0)] * n`, `"ab" * 3`, `b"\x00" * 1024`. There is no operation that changes an integer, a string, `None` or a tuple of immutables in place, so it is impossible to observe that the slots share. Pre-allocating with `[None] * n` and then assigning into the slots is a normal, efficient Python idiom: the assignment `buf[i] = value` rebinds a slot, it does not mutate the shared `None`. **Unsafe case — mutable elements.** `[[]] * n`, `[{}] * n`, `[set()] * n`, `[bytearray(4)] * n`, `[SomeClass()] * n`, and `[obj] * n` where `obj` is a mutable object bound earlier. Here the left operand is evaluated exactly once, before repetition happens, so exactly one object exists no matter how large `n` is. Any in-place mutation through one slot is visible through all of them. ### The decision rule Ask two questions in order: 1. **Can the element be mutated in place at all?** If it is an immutable built-in, stop — repetition is fine and is the fastest way to build the sequence. 2. **Will anything mutate it?** Even a mutable element is harmless if the slots are only ever rebound and never mutated. This is fragile, though: it depends on every present and future caller, so it is not a safety argument worth relying on in shared code. If either answer says the element is mutable and gets mutated, you need per-slot construction. ### Building independent elements ```python rows = [[] for _ in range(4)] assert len({id(r) for r in rows}) == 4 buffers = [bytearray(4) for _ in range(4)] buffers[0][0] = 1 assert buffers[1][0] == 0 ``` The comprehension is not magic — it simply re-evaluates its expression on every iteration of its loop, so a new object is constructed each time. The same reasoning explains a subtler mistake: `[make_row()] * n` is still broken, because `make_row()` is called once and only its result is repeated. The call has to be **inside** the comprehension: `[make_row() for _ in range(n)]`. When the elements come from an existing sequence rather than a literal, rebuild each one: `[row[:] for row in template_rows]` or `[list(row) for row in template_rows]` gives you a new outer list *and* new inner lists. Copying only the outer sequence duplicates the pointer array while leaving every pointer aimed at the original inner objects, which is why an outer-level copy never fixes an aliasing bug one level down. ### Why it survives testing The aliased and the correct construction compare equal immediately after creation, so an assertion on the freshly built value passes in both cases. The divergence only appears after the first in-place mutation, which in real code often happens in a different function, in a later pass, or on a later request. That gap between the defective line and the visible symptom is why this trap keeps catching experienced engineers, and why the review-time heuristic matters more than the debugging skill: mutable literal or call on the left of `*` is the whole signal. ### Immutable containers are not an exemption `([],) * 3` builds a tuple of three references to one list. The tuple cannot be reassigned, but the list it points at can still be mutated, and the mutation is visible through all three positions. Immutability of the container says nothing about the objects inside it. ### Performance is a real reason to use repetition Where the element is immutable, repetition is genuinely the right tool: it allocates the slot array once and fills it without running Python-level loop machinery, so `[None] * 1_000_000` is substantially faster than the equivalent comprehension. Choosing repetition for immutable fills and comprehensions for mutable elements gets both the correctness and the speed right.

  • Is ([], [], []) safer than ([],) * 3 because a tuple is immutable?
    The tuple's immutability only prevents rebinding its positions. `([],) * 3` still holds one list referenced three times, so appending through position 0 is visible at positions 1 and 2. The literal `([], [], [])` evaluates three separate displays and therefore holds three distinct lists. Container immutability says nothing about the objects inside.
  • Why is [None] * n still worth using instead of a comprehension?
    It is correct — `None` cannot be mutated, so sharing is unobservable — and it is faster, because it allocates and fills the slot array in one step instead of running a Python-level loop. Pre-allocating with `[None] * n` and then assigning into the slots is standard practice for a fixed-size buffer.
  • Does [make_row()] * n call make_row once or n times?
    Once. The whole left operand is evaluated before repetition happens, so exactly one row object exists and every slot references it. To get n independent rows the call must be inside a comprehension: `[make_row() for _ in range(n)]`, which re-evaluates the call on each iteration.

saying these in an interview costs you the question

  • Says repetition is always unsafe, avoiding [None] * n
  • Thinks a call on the left runs once per repetition
  • Believes an immutable container protects the objects inside
  • Cannot state that immutable elements make sharing unobservable
  • Claims a comprehension is slower so repetition is preferable everywhere

context