Why does `lst[:] = items` behave differently from `lst = items` for other holders of that list?
answer
- One touches a name, one an object
- What a second reference to the list sees
- Which form calls a method on the container
- clear(), del lst[:] and lst[:] = [] agree
basics
~20 slst = 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.
solid answer
~50 sAssignment to a bare name is a **binding** operation: it points that one name at a different object and changes nothing about the old one. Assignment to a slice is a **mutation**: `lst[:] = items` goes through the list's item-assignment machinery, drops the current elements and splices in the items of any iterable, keeping the same object identity. So after `alias = lst`, only the slice form is visible through `alias`. The related in-place forms are `del lst[i]`, `del lst[a:b]` and `lst.clear()` (equivalent to `del lst[:]`); `lst += items` mutates too, unlike `lst = lst + items`, which rebinds. A plain slice may change the length, while an **extended** slice with a step requires a right-hand side of exactly matching length. Do not treat the swap as atomic across threads: a free-threaded 3.14 build has no GIL to serialize it.
code
python · 9 linesshared = [1, 2, 3]
alias = shared
shared[:] = [9, 9] # in-place: the shared object itself changes
print(alias) # [9, 9]
shared = [0] # rebinds the name only
print(alias) # [9, 9] -- alias still holds the old object
print(shared is alias) # Falsego deeper
Know that assigning to a name never changes the object that name used to point at. If you need a second holder of a list to see the change, you must mutate the list rather than rebind the name.
Explain the mechanics: a bare-name target binds, a subscript or slice target calls into the container; slice assignment accepts any iterable and may change the length, while clear(), del lst[:] and lst[:] = [] are equivalent.
Show the production judgement: identify every holder of a shared list before choosing between the forms, and recognise the failure where workers keep appending to a list the coordinator has rebound away from.
Own the design position: shared mutable lists across threads or modules are the defect surface here, so prefer ownership boundaries, returned values or immutable snapshots over relying on in-place replacement being observed correctly.
### Names bind, subscripts mutate Python's assignment statement does two entirely different things depending on the target. `name = value` rebinds a name in a namespace — no method on the old object is called, and the old object is completely unaffected. `target[key] = value` is not a binding at all: it is a method call on the container, which is free to change itself. That single distinction explains the whole question. ```python shared = [1, 2, 3] alias = shared shared[:] = [9, 9] # mutates the object both names point at print(alias) # [9, 9] shared = [0] # rebinds the name only print(alias) # [9, 9] -- alias still holds the previous object ``` ### Why it matters in real code Any time a list has more than one holder, the two forms diverge. Picture a coordinator that hands a shared results list to a pool of workers processing a geocoding batch; each worker appends the addresses it resolves. If the coordinator later “resets” the batch with `results = []`, the workers keep appending to the *old* list object, which the coordinator no longer reads. Nothing raises. The batch simply reports zero results, intermittently, depending on when the reset landed — a race on shared state whose root cause is a one-character difference between `results = []` and `results[:] = []`. The same shape appears with a module-level configuration list, a list captured by a closure, or a list held by an object attribute. The rule that follows is worth stating plainly: **if a list is shared, mutating it in place and rebinding the name are different operations with different audiences, and you must choose deliberately.** ### The full in-place vocabulary - `lst[i] = x` replaces one element. - `lst[a:b] = iterable` splices: the target range is removed and the items of the iterable are inserted, so the length may change. The right-hand side may be *any* iterable — a generator, a string, a set — not just a list. - `lst[a:b:step] = iterable` — an *extended* slice — cannot change the length: the iterable must supply exactly as many items as the slice selects, or a `ValueError` is raised. - `del lst[i]` and `del lst[a:b]` remove elements in place. - `lst.clear()` empties the list in place; it is equivalent to `del lst[:]` and to `lst[:] = []`, and has existed since 3.3. - `lst += items` is in-place too — it routes through `extend`, so inside a function it mutates the caller's list, while `lst = lst + items` rebinds the local name and leaves the caller's list alone. ### Cost Slice assignment is O(n) in the elements that move: replacing the whole list touches every slot and releases every old element's reference. Replacing a prefix shifts the untouched tail. It is not a free swap — for a large list, rebinding is the cheap operation and in-place replacement is the one you pay for, which is exactly the opposite of the intuition that “in place must be cheaper”. ### Concurrency caveat `lst[:] = items` is a single statement, and it is tempting to treat it as an atomic swap that other threads can never observe half-done. Do not. Materializing the right-hand side iterates it, which can execute arbitrary Python code; dropping the replaced elements can run their finalizers; and on a free-threaded build — experimental in 3.13 and officially supported in 3.14 under PEP 779 — there is no global lock serializing bytecode at all. If several threads read and write the same list, protect it with a `threading.Lock`, or hand each worker its own list and merge at the end. ### The interview signal This question separates candidates who have internalized Python's names-and-objects model from those who read assignment as “put the value in the variable”. The follow-up is usually the function-argument version: why `def f(x): x = []` cannot clear the caller's list while `def f(x): x[:] = []` can. Both are the same rule seen twice.
- Inside a function that received a list argument, how does `lst += other` differ from `lst = lst + other`?`+=` is in-place: it routes through `extend`, mutates the object the caller passed in, and the caller sees the added items. `lst = lst + other` builds a new list and rebinds the local parameter name, so the caller's list is untouched. The two lines look almost identical and have opposite effects on the caller — a frequent source of confusing bug reports.
- Can you rely on `lst[:] = items` being atomic across threads?No. Materializing the right-hand side iterates it, which can run arbitrary Python code, and releasing the replaced elements can run finalizers, so another thread can observe an intermediate state. On a free-threaded build — officially supported in 3.14 — there is no global lock serializing bytecode at all. Guard a shared mutable list with a `threading.Lock`, or give each worker its own list.
- What constraint applies when assigning to an extended slice such as `lst[::2]`?The right-hand side must supply exactly as many items as the slice selects, because an extended slice cannot change the list's length — a mismatch raises `ValueError`. A plain slice with no step has no such restriction: `lst[1:3] = ["a", "b", "c"]` happily replaces two elements with three and lengthens the list.
Rebinding is redirecting your own bookmark to a different document; slice assignment is rewriting the document everyone else has open.
saying these in an interview costs you the question
- Thinks `lst = []` empties the list for every holder
- Believes slice assignment and rebinding are the same operation
- Says the right-hand side of a slice assignment must be a list
- Assumes extended-slice assignment can change the length
- Treats in-place list mutation as thread-safe by default