skip to content

Why does `b = a` on a Python list leave both names pointing at one object?

level: juniorimportance: must knowfreq 76%

answer

  1. One object, two labels
  2. The equals sign duplicates nothing
  3. Mutating differs from rebinding a name
  4. Arguments bind, they do not copy
  5. Ask for a copy explicitly

basics

~20 s

Assignment never copies in Python; it binds a second name to the same object. After b = a both names label one list, so b.append(4) is visible through a. Duplicate explicitly with a.copy(), a[:] or list(a).

solid answer

~50 s

In Python an object lives in memory and a name is just a binding that refers to it. `b = a` evaluates the right-hand side to an object and binds `b` to **that same object** — no elements are duplicated, only the reference count goes up. So mutating through either name (`b.append(4)`, `b[0] = 9`, `b.sort()`) is observable through the other, while *rebinding* (`b = [9]`) affects only the name `b` and leaves `a` alone. The same binding rule applies to function arguments, `for` targets and anything stored in a container or attribute: the reference is stored, not a snapshot. With immutable objects such as `int`, `str` and `tuple` the sharing is invisible because nothing can mutate them. When you actually need an independent list, ask for one: `a.copy()`, `a[:]`, `list(a)`, or `copy.copy(a)` for arbitrary objects.

code

pycon · 12 lines
pycon
>>> a = [1, 2, 3]
>>> b = a
>>> b.append(4)
>>> a
[1, 2, 3, 4]
>>> c = a.copy()
>>> c.append(5)
>>> a
[1, 2, 3, 4]
>>> b = [9]
>>> a
[1, 2, 3, 4]

go deeper

for a junior

Be ready to say that assignment binds a name to an existing object and never duplicates it, and to name list.copy(), slicing or list() as the explicit ways to get a second list.

for a middle

Explain mutation versus rebinding precisely, and show that argument passing binds the parameter to the caller's object, so mutating it is visible but rebinding it is not.

for a senior

Demonstrate the ownership habit in real code: copy or freeze containers that cross an API boundary, and trace an unexpected mutation back to the exact binding that shared the reference.

for a principal

Own the convention across a codebase: decide whether state is defended by copying at boundaries or by immutable value types, and make it consistent so reviewers do not re-litigate ownership case by case.

### Names are labels, objects are the things Python's execution model separates two ideas that many languages blur. An **object** is a thing that exists in memory: it has a type, a value and an identity. A **name** is an entry in a namespace — a module's globals, a function's locals, an instance's `__dict__` — that refers to an object. The assignment statement `b = a` evaluates the right-hand side to an object and binds the left-hand name to *that same object*. Nothing is duplicated: no list cells, no dict buckets, no bytes. The only thing that changes is that one more name refers to the object, and CPython increments its reference count. That is why the classic surprise happens: ```python a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4] ``` Nobody touched `a`, and yet `a` changed — because there was never a second list to begin with. There is one list object with two labels on it. ### Mutation versus rebinding Two operations look similar on the page and behave completely differently. * `b.append(4)`, `b[0] = 9`, `b.sort()` **mutate** the object both names refer to. Every name bound to that object observes the change. * `b = [9]` **rebinds** the name `b` to a brand-new list. `a` is untouched, because rebinding a name never reaches through to the object the name used to hold. Binding happens in far more places than the `=` sign. Passing an argument binds the parameter name to the caller's object — often described as *call by object reference* or *call by sharing*. A `for` target binds each item in turn. Storing an object in a list, dict or attribute stores the reference. All of them share rather than copy: ```python def add_rule(rules): rules.append("device") # mutates the caller's list def replace_rules(rules): rules = ["device"] # rebinds a local name; caller sees nothing ``` ### Why immutable objects hide the rule A beginner writes `x = 5; y = x; y += 1`, sees `x` unchanged, and concludes that assignment copies. It does not. `int`, `str`, `tuple`, `frozenset` and `bytes` simply have no mutating operations, so sharing one of them is unobservable — `y += 1` had to build a new integer and rebind `y`. The moment a mutable object is shared — a `list`, `dict`, `set`, `bytearray` or an ordinary class instance — the same binding rule becomes visible. The rule never changed; only its observability did. ### Asking for a copy Because assignment never copies, duplication must be requested explicitly: * a list: `list(a)`, `a[:]`, `a.copy()` * a dict: `d.copy()` or `dict(d)` * a set: `s.copy()` or `set(s)` * anything, including your own classes: `copy.copy(obj)` from the `copy` module Each of those produces a **new outer container holding the very same inner objects** — a shallow copy. That is exactly right when the elements are immutable and not enough when they are not; `copy.deepcopy(obj)` walks the whole graph instead. One trap worth memorising: `list(d)` on a dict does not copy the dict at all, it builds a list of its keys. ### Where it bites in real code The most common production shape is an object that hands out its own internal container: ```python class RuleSet: def __init__(self): self._rules = ["velocity", "geo"] def rules(self): return self._rules # the caller can now mutate our state ``` Every caller can `append` into private state, and the resulting bug surfaces far from the code that caused it. Returning `list(self._rules)` or `tuple(self._rules)` makes ownership unambiguous. The mirror image is a constructor that stores a caller's list — `self.items = items` — and is then surprised when the caller keeps mutating it afterwards. Copy on the way in, copy on the way out, or use an immutable value. ### Diagnosing it The symptom is action at a distance: a value changes although nothing nearby touched it. Confirm the alias by comparing identity — `a is b` is true exactly when the two names refer to one object — or by printing `id(a)` and `id(b)`. Then trace backwards to the binding that shared the reference: a bare assignment, an argument pass, a value stored in two places, or a container holding the same object twice. ### The mental model worth keeping Read every assignment aloud as *"bind this name to that object"*, never as *"put the value into the variable"*. Once the second phrasing is gone, aliasing, mutable default arguments, list-of-lists sharing and the whole shallow-versus-deep copy question stop being separate surprises and become one rule showing up in different places.

  • If assignment does not copy, how would you describe what happens when you pass a list to a function?
    The parameter name is bound to the caller's object, so the function and the caller share it. Mutating it through the parameter (`append`, item assignment, `sort`) is visible to the caller; rebinding the parameter to a new list is not, because that only changes the function's local name. Passing an immutable object makes the distinction invisible, since nothing can mutate it in place.
  • How do you stop callers from mutating a list that a method returns?
    Return a copy or an immutable view: `list(self._rules)` gives the caller its own list, and `tuple(self._rules)` gives one they cannot mutate at all. The same discipline applies on the way in — if a constructor stores a caller's list directly, copy it there, otherwise the caller keeps a live handle on your internal state.
  • Why does `y = x; y += 1` leave `x` unchanged for integers but not the analogous code for lists?
    Integers are immutable, so `y += 1` cannot modify the object in place; it computes a new integer and rebinds `y`, leaving `x` on the old one. A list implements in-place addition, so the same syntax mutates the shared list and both names see the result. The binding rule is identical in both cases; only the object's mutability differs.

Assignment is putting a second sticky label on one box, not fetching a second box. Writing a new label onto the sticky note moves only that label; opening the box and adding an item is seen by everyone holding a label for it.

saying these in an interview costs you the question

  • Says the assignment operator makes a copy of the list
  • Believes passing a list to a function copies it
  • Uses b = a expecting an independent snapshot
  • Confuses rebinding a name with mutating the object
  • Thinks id() differing proves the values differ

context