Names, references and mutability

Python · Essentials · 10 min

This mental model explains aliasing bugs, default-argument surprises and how function arguments behave.

In Python, assignment binds a name to an object; it never copies. After b = a, both names refer to the same object. Whether that matters depends on mutability.

  • Immutable: int, float, str, tuple, frozenset, bytes. Operations create new objects, so sharing is harmless.
  • Mutable: list, dict, set, most user classes. Changing the object in place is visible through every name that refers to it.

So b = a; b.append(4) changes a too, while b = a; b = b + [4] rebinds b to a new list and leaves a alone. id(x) returns an object's identity, and a is b asks whether two names point at the same object.

Function arguments follow the same rule ("call by object reference"): the parameter is a new name for the caller's object. A function that mutates a list argument changes the caller's list; a function that rebinds its parameter does not affect the caller.

To get an independent copy, ask for one:

  • items.copy(), list(items) or items[:] make a shallow copy — a new outer list holding the same inner objects
  • copy.deepcopy(obj) recursively copies nested structures

The classic trap is grid = [[0] * 3] * 3: the outer * repeats the reference to one inner list three times, so setting grid[0][0] changes every row. Build it with a comprehension instead: [[0] * 3 for _ in range(3)].

Example

import copy

a = [1, 2, 3]
b = a                 # same object
b.append(4)
print(a, a is b)      # [1, 2, 3, 4] True

c = a.copy()          # shallow copy
c.append(5)
print(a, c)           # [1, 2, 3, 4] [1, 2, 3, 4, 5]
def add_item(lst):
    lst.append("x")   # mutates the caller's list
add_item(a)
print(a)              # [1, 2, 3, 4, 'x']
bad = [[0] * 2] * 2
bad[0][0] = 9
print(bad)            # [[9, 0], [9, 0]]

good = [[0] * 2 for _ in range(2)]
good[0][0] = 9
print(good)           # [[9, 0], [0, 0]]
nested = {"tags": ["a"]}
deep = copy.deepcopy(nested)
deep["tags"].append("b")
print(nested)         # {'tags': ['a']}

Remember

  • Assignment binds names; it never copies
  • Mutating an object is visible through every name bound to it
  • Use .copy() for a shallow copy and copy.deepcopy for nested data

Watch out for

  • Creating a 2-D list with [[0] * n] * m, which aliases every row.

Check yourself

  1. After `x = [1]; y = x; y = y + [2]`, what is `x`?
  2. After `x = [1]; y = x; y += [2]`, what is `x`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/reference/datamodel.html#objects-values-and-types