Why is `[0]*n` safe in Python while `[[]]*n` shares one list?
answer
- Ask whether the element can change
- Repetition duplicates slots, not objects
- Rebinding a slot versus mutating an object
- Immutable fillers make sharing unobservable
- An immutable tuple can hold a list
basics
~20 sRepetition always copies references, never objects. Nothing can mutate an integer, so sharing 0 is unobservable and xs[0] = 9 merely rebinds a slot. With [[]]*n every slot references one list, so xs[0].append(1) shows up everywhere.
solid answer
~40 s`seq * n` fills the new sequence's slots with the references already in the operand's slots; it never copies the objects at any depth. So the elements are always shared — the question is whether the sharing is *observable*, and it is observable only when something can change the shared object in place. `xs[0] = 9` is an index assignment: it changes the list so that slot 0 references a different object, leaving the other slots alone. `xs[0].append(1)` is a method call on the element: it changes the one object every slot references. Integers, strings, `None`, tuples of immutables and `frozenset` offer nothing of the second kind, so `[0]*n` and `[None]*n` are idiomatic preallocation. Lists, dicts, sets and ordinary instances do, so repeat them only when you genuinely want one shared object.
code
python · 8 linesxs = [0] * 3
xs[0] = 9
print(xs) # [9, 0, 0] - slot 0 was rebound
ys = [[]] * 3
ys[0].append(1)
print(ys) # [[1], [1], [1]] - one shared list
print(len({id(y) for y in ys})) # 1go deeper
Recall the practical rule: [0]*n and [None]*n are the normal way to preallocate a list, but the moment the repeated element is a list, dict or set, build the elements with a comprehension instead.
Explain it in terms of slots: xs[i] = v rebinds slot i, while xs[i].append(v) mutates the object every slot references. Say why immutability makes the sharing unobservable rather than absent.
Extend the rule past the obvious cases: a tuple element can hold a mutable one, and any object with in-place methods repeats the trap. Show how you would encode 'n independent containers' in a construction helper others cannot get wrong.
Frame it as API design: a function handing back n containers should take a factory rather than an instance, so callers cannot accidentally share state. Decide where that convention is worth enforcing and where it is ceremony.
### One rule, two outcomes Sequence repetition has a single rule: `seq * n` builds a new sequence whose slots hold the references already in `seq`'s slots. It never copies the objects behind those references, at any depth. So the question is never *whether* the resulting elements are shared — with `n > 1` and a one-element operand they always are — but whether that sharing is *observable*. Sharing becomes observable only when something can change the shared object in place. That is the whole of the immutable/mutable split. ### Rebinding a slot versus mutating an object Two operations look similar and are not: * `xs[0] = 9` — an index assignment. It changes the *list*: slot 0 now references a different object. Slots 1 and 2 still reference whatever they referenced before. The old object is untouched. * `xs[0].append(1)` — a method call on the *element*. It changes the object that slot 0 references. Every other slot referencing that same object sees the change, because there is only one object. With `xs = [0]*3` you can only ever do the first kind of thing, because `int` exposes no in-place mutation at all: arithmetic returns new integers. So `[0]*3` behaves exactly as if the zeros had been copied, and CPython gets to skip the copying. With `ys = [[]]*3` the second kind is available and is usually what you want to do with a list, which is why the trap fires almost immediately. ### The safe fillers Elements that are immutable all the way down are always safe to repeat: `int`, `float`, `bool`, `str`, `bytes`, `None`, `frozenset`, `range`, and tuples whose own elements are immutable. `[None]*n` and `[0]*n` are the standard way to preallocate a list of known length, and `['-']*width` is the standard way to build a placeholder row. Repeating an immutable *sequence* is safe for a second reason: `'ab'*3` and `(0,)*3` produce a brand-new `str` or `tuple`, and neither the result nor its elements can be mutated, so no aliasing is observable anywhere. ### The unsafe fillers Anything with in-place mutation is unsafe to repeat when you meant 'n independent ones': `list`, `dict`, `set`, `bytearray`, `collections.deque`, `collections.Counter`, and ordinary class instances with settable attributes. `[[]]*n`, `[{}]*n`, `[set()]*n` and `[MyState()]*n` all give you one object under n names. The subtle one is the tuple that is immutable but not *deeply* immutable: `([],)*3` yields three references to a single tuple whose element is a mutable list, so `t[0][0].append(1)` is visible through all three. 'Immutable container' is not the same claim as 'immutable contents'. ### `+` is the same shallowness `xs + ys` also builds a new list holding the existing element references — the outer list is new, the elements are shared. `xs + xs` therefore places each element of `xs` in the result twice. It differs from `xs * 2` only in that you wrote the operand twice; if the operand expression is *evaluated* twice, as in `[[0]*3] + [[0]*3]`, you get two distinct rows, and that is a difference in evaluation, not in the operator's copying behaviour. Augmented assignment is a different question again: `xs = xs * 2` builds a new list and rebinds the name, while `xs *= 2` calls the in-place multiply and extends the existing list, so every other name bound to that list sees it double in length. ### The rule to carry Ask one question at the construction site: *can the repeated element be changed in place?* If no, repetition is a fast, idiomatic preallocation and you should use it. If yes, you almost certainly wanted n independent objects, and only a per-item construction — a comprehension, a loop with `append`, or a factory such as `collections.defaultdict(list)` for the dict case — gives you that. Stating the rule in those terms is what an interviewer is listening for, because it generalises to every place Python hands you a shared reference rather than a copy.
- Is `[()]*n` or `[frozenset()]*n` ever a problem?No: both elements are immutable, so nothing can change what a slot references except an assignment to that slot, and the sharing is unobservable. Watch the nested case, though — a tuple is immutable but may *contain* a list, so `([],)*3` gives three references to one tuple whose element is very much mutable.
- Does `'ab'*3` or `(0,)*3` create aliasing you should worry about?No. Repeating an immutable sequence produces a brand-new `str` or `tuple`, and neither the result nor its elements can be mutated in place, so there is nothing to observe. The aliasing question only arises when the repeated *element* is mutable.
- How does `xs *= 2` differ from `xs = xs * 2` for a list?`xs = xs * 2` builds a new list and rebinds the name, so other names still see the original length. `xs *= 2` calls `list.__imul__`, which extends the existing list in place, so every name bound to that list sees it double. Element-level shallowness is identical either way.
saying these in an interview costs you the question
- Says `*` makes a shallow copy of each element
- Thinks integers are safe because Python copies numbers
- Believes one slot assignment can corrupt `[None]*n`
- Assumes any tuple element is safe to repeat
- Claims `'ab'*3` shares a string that could change
- Treats `xs + xs` as producing independent elements