Mutability and Aliasing

Two names pointing at one list, why changing one changes both, and the difference between rebinding a name and mutating an object.

Overview

Assignment does not copy

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

b = a attaches a second label to the same list. There is one list and two names for it, so a change through either name is visible through both. a is b is True, which is the test for "the same object" as opposed to ==, which asks about contents.

To get a second list, ask for one: a[:], list(a) or a.copy().

aliasing.py

aliasing.py Python 3
Output

                    

mutable_arguments.py

mutable_arguments.py Python 3
Output

                    

Worth knowing

b = a gives the object a second name. It does not copy anything.
is asks "the same object?"; == asks "the same contents?".
Rebinding a name inside a function is local. Mutating the object is visible to the caller.
[[0]*3]*3 repeats one inner list three times. Use a comprehension.

Mutability and Aliasing: A Practical Guide

A name in Python is a label attached to an object, not a box holding a value. Once that clicks, a whole family of confusing behaviour becomes obvious.

Rebinding versus mutating

This is the distinction that explains the rest:

y = [9, 9]    # rebinding: point y at a different object
y.append(3)  # mutating: change the object y points at

Rebinding affects only that name. Mutating affects every name pointing at that object. Both use y, which is why they look similar and behave nothing alike.

Inside functions

def add_zero(items):
    items.append(0)  # caller sees this

def replace(items):
    items = [9, 9]    # caller sees nothing

The parameter is another name for the caller's object. Mutate it and the caller's list changes. Rebind it and you have only pointed the local name elsewhere.

This is not "pass by reference" or "pass by value" — it is simply the same naming rule as everywhere else in the language.

Immutable types dodge the question

Numbers, strings and tuples cannot be mutated, so there is no way for one name to change what another sees. n += 1 inside a function must rebind, because there is no other option. That is why the whole issue only ever comes up with lists, dicts and sets.

The multiplication trap

grid = [[0] * 3] * 3
grid[0][0] = 1  # every row changes

[0] * 3 builds one row. Multiplying the outer list by 3 does not build three rows — it stores three references to the same row. Setting one cell appears to set three.

grid = [[0] * 3 for _ in range(3)]

The comprehension evaluates [0] * 3 afresh each pass, so there really are three lists. This is the same rule as the mutable default argument: one object created once, shared everywhere.

Check yourself

0 of 3

Answer without scrolling back up.

  1. After `a = [1]; b = a; b.append(2)`, what is `a`?

  2. A function does `items = [9]`. What does the caller see?

  3. Why does `[[0]*3]*3` misbehave?

Cheat sheet

Mutability and Aliasing

A name in Python is a label attached to an object, not a box holding a value. Once that clicks, a whole family of confusing behaviour becomes obvious.

PYTHON · vizlearn.in/python/mutability_and_aliasing.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.