skip to content

Mutability, Identity and Copying

How names, objects and copies relate: what `is` means next to `==`, which types can be hashed, and why a copy often still shares nested state. Nearly every classic Python gotcha lives here.

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

questions

20

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

open as a page

What is the difference between `is` and `==` in Python?

level: juniorimportance: must knowfreq 88%

basics

~20 s

is asks whether two names are bound to one and the same object, the identity test behind id(). == asks whether the objects compare equal, dispatching to eq. Compare values with ==; reserve is for None, True, False and sentinels.

open as a page

Why does `lst += [4]` change the list other names see, but `n += 1` does not?

level: juniorimportance: must knowfreq 62%

basics

~20 s

+= on a list calls list.__iadd__, which extends that same list object in place, so every name bound to it sees the new items. An int is immutable: n += 1 builds a new int and rebinds only the name n.

open as a page

Which of Python's built-in types are immutable, and which can be changed in place?

level: juniorimportance: must knowfreq 78%

basics

~20 s

int, float, bool, str, bytes, tuple and frozenset are immutable: every apparent change builds a new object. list, dict, set and bytearray are mutable and can be modified in place through any name that refers to them.

open as a page

Why does `dict.copy()` leave nested lists shared with the original dict?

level: middleimportance: must knowfreq 64%

basics

~20 s

dict.copy() is shallow: it builds a new dict whose values are the very same objects as the original's. Replacing a key in the copy is independent, but mutating a nested list is seen by both dicts. copy.deepcopy duplicates the nested objects too.

open as a page

Why does defining `__eq__` on a class make its instances unhashable?

level: middleimportance: must knowfreq 55%

basics

~20 s

Because Python sets hash to None on any class that defines eq without also defining hash. The inherited identity hash would disagree with your new equality, and equal objects must hash equally, so hashing is disabled until you decide what it should be.

open as a page

Why does `t[0] += [2]` on `t = ([1],)` raise TypeError yet still extend the list?

level: middleimportance: must knowfreq 48%

basics

~20 s

The list inside the tuple is extended first by list.__iadd__; augmented assignment then stores the result back with t[0] = ..., and a tuple rejects item assignment. The TypeError is raised after the mutation has already happened.

open as a page

Why does `def record_bid(price, log=[])` keep values from earlier calls?

level: middleimportance: must knowfreq 80%

basics

~20 s

The empty list is built once, when the def statement executes at import time, and stored on the function object. Every call that omits log mutates that same list. Default to None and build a fresh list inside the body.

open as a page

Which strings does CPython intern automatically, and what does `sys.intern` do?

level: middleimportance: should knowfreq 48%

basics

~20 s

CPython keeps one canonical copy of certain strings in an intern table: identifier-like string constants in compiled code, plus names, attribute names, the empty string and single characters. sys.intern forces a string into that table and returns the canonical object.

open as a page

Why can a tuple be used as a dict key when a list cannot?

level: middleimportance: should knowfreq 62%

basics

~20 s

Dict keys and set members must be hashable, and Python only makes objects hashable when their value is fixed. list, dict and set set __hash__ to None, so hash([1, 2]) raises TypeError; tuple, frozenset, str and the numeric types hash fine.

open as a page

How does copy.deepcopy's memo handle shared references and reference cycles?

level: seniorimportance: should knowfreq 40%

basics

~20 s

copy.deepcopy keeps a memo dictionary mapping each already-copied object's id() to its new copy. A second encounter with the same object reuses that copy, so aliasing in the original is reproduced in the result and reference cycles terminate instead of recursing forever.

open as a page

Why do Python libraries use a module-level sentinel object with `is` checks?

level: seniorimportance: should knowfreq 35%

basics

~20 s

To tell an omitted argument from one explicitly passed as None, when None is itself a valid value. A unique module-level marker is compared with is, which no class can override, cannot raise, and costs nothing.

open as a page

Why can an `is` comparison of parsed codes pass every test yet silently drop rows in production?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Test fixtures use small literals that CPython shares — cached integers up to 256 and interned identifier-like strings — so identity holds. Real data arrives as run-time strings and larger integers, which are distinct objects, so every identity check quietly returns False.

open as a page

A route-optimisation worker runs `self.routes += batch` while another thread reassigns `self.routes`; what breaks?

level: seniorimportance: should knowfreq 30%

basics

~20 s

self.routes += batch is three steps: load the attribute, extend the list in place, store it back. A concurrent self.routes = [] landing between them is silently undone, because the store rebinds the attribute to the old, now-extended list.

open as a page

Why is `a is b` True for `a = 256; b = 256` but False for 257 in CPython?

level: juniorimportance: nice to knowfreq 24%

basics

~20 s

CPython pre-allocates one shared int object for every value from -5 to 256, so all names holding 256 point to that one object. 257 is built fresh each time, so two such names hold equal but distinct objects.

open as a page

Why does `1 == True` return True in Python?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Because bool is a subclass of int: True and False are integers with the values 1 and 0. They therefore compare equal to those integers, hash the same, and can be used in arithmetic — but True and 1 are still different objects.

open as a page

Why can `a is b` for two equal string literals differ between the Python REPL and a script?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

The compiler stores each distinct constant once per compiled code object, so two equal literals in one script share an object. The interactive interpreter compiles each statement separately, so each line gets its own constant and identity fails.

open as a page

What must `__iadd__` return, and what happens if a class only defines `__add__`?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

__iadd__ must return the object to bind, normally self after mutating in place. Omit the return and the name is rebound to None. With only __add__ defined, += falls back to it and rebinds the name.

open as a page

When do you implement `__deepcopy__` on a class, and what must it do with memo?

level: seniorimportance: nice to knowfreq 17%

basics

~20 s

Implement deepcopy(self, memo) when the default copy is wrong: the object holds a lock, socket or handle that must not be duplicated, or a cache to drop. Register the new object in memo before recursing, and pass memo down to nested copies.

open as a page

A scorer wrapped in functools.lru_cache raises TypeError: unhashable type: 'dict' — why, and how do you fix it?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

The cache keys entries by the call's arguments, so every argument must be hashable, and a dict is not. Pass a canonical immutable form instead — a frozenset of the mapping's items, or a sorted tuple of them — and unpack it inside.

open as a page