Skip to content

Mutability and references

4 min read

In lesson 1.1 you learned that a name refers to a value, like a tag hanging on it, not a box holding it. With lists, that difference explains bugs that confuse even experienced programmers. By the end of this lesson you'll predict them, and make a real copy when you need one.

Two names, one list

Predict what this prints.

What will this print?

Decide before you look. Guessing wrong is how this sticks.

a = [1, 2]
b = a
b.append(3)
print(a)
Pick the output

Python prints

[1, 2, 3]

b = a doesn't copy the list; it gives the same list a second name. Appending through b changes that one list.

If you said [1, 2], you pictured b = a making a second list. Step through what really happens: each arrow shows which value a name refers to.

Names and objects

Press Next to run one line at a time. Each arrow shows the object a name refers to.

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

a refers to a list, [1, 2].

Step 1 of 4

Step 1 of 4. Line 1: a = [1, 2]. a now refers to a new list, [1, 2].

Python calls each value an . After line 2 there is one list object with two names on it. So the rule is: assignment never copies. b = a means "b now refers to the object a refers to". The two names aren't tied together; they just share one object. Now a subtle one:

Quick check

After x = [1], then y = x, then y = [2], what is x?

Moving a name doesn't change the list

Press Next to run one line at a time. Each arrow shows the object a name refers to.

x = [1]
y = x
y = [2]

x refers to a list, [1].

Step 1 of 3

Step 1 of 3. Line 1: x = [1]. x now refers to a new list, [1].

Changing an object and moving a name are different things. y.append(2) or y[0] = 2 would change the list, and x would see it. y = [2] only moves the name y.

The same object, or equal?

== asks "same value?". is (from lesson 2.4) asks "same object?". Here c is a separate list with the same numbers:

a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == c)
print(a is b)
print(a is c)

In the editor, press Escape then Tab to move on.

Two equal lists are == but not is. Compare values with ==; keep is for None, or for when you really mean "the same object".

Why numbers and strings never surprise you

Now the same shape of program with a string. Predict:

What will this print?

Decide before you look. Guessing wrong is how this sticks.

s = "ab"
t = s
t += "c"
print(s)
Pick the output

Python prints

ab

A string can't change, so t += "c" builds a new string, "abc", and moves t to it. s still refers to the original.

Names and objects

Press Next to run one line at a time. Each arrow shows the object a name refers to.

s = "ab"
t = s
t += "c"
print(s)

s refers to a str, 'ab'.

Step 1 of 4

Step 1 of 4. Line 1: s = "ab". s now refers to a new str, 'ab'.

Strings and numbers can't change, so sharing one never surprises you: t += "c" and n += 1 make new objects.

A list can change. Predict this one:

What will this print?

Decide before you look. Guessing wrong is how this sticks.

a = [1, 2]
b = a
b += [3]
print(a)
Pick the output

Python prints

[1, 2, 3]

On a list, += changes the list in place, like extend, and a refers to that same list.

+= on a list

Press Next to run one line at a time. Each arrow shows the object a name refers to.

a = [1, 2]
b = a
b += [3]
print(a)

a refers to a list, [1, 2].

Step 1 of 4

Step 1 of 4. Line 1: a = [1, 2]. a now refers to a new list, [1, 2].

So b += [3] and b = b + [3] are not the same for lists: + builds a new list and moves b to it. Run this, then change line 3 to b = b + [3] and run it again: a stays [1, 2].

a = [1, 2]
b = a
b += [3]
print(a)

In the editor, press Escape then Tab to move on.

The grid bug

You want a 3 by 3 grid of zeros, then set the top-left square to 1. Predict.

What will this print?

Decide before you look. Guessing wrong is how this sticks.

grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
Pick the output

Python prints

[[1, 0, 0], [1, 0, 0], [1, 0, 0]]

There's only one row, in the grid three times, so changing it shows in every row.

[row] * 3 doesn't copy the row. It makes a list whose three slots all refer to the same row:

Names and objects

Press Next to run one line at a time. Each arrow shows the object a name refers to.

grid = [[0] * 3] * 3
grid[0][0] = 1

grid refers to a list, [R1, R1, R1]: all 3 slots refer to one list, [0, 0, 0].

Step 1 of 2

Step 1 of 2. Line 1: grid = [[0] * 3] * 3. grid now refers to a new list, [R1, R1, R1]: all 3 slots refer to one list, [0, 0, 0].

[0] * 3 repeats the number 0 the same way, but numbers can't change: grid[0][0] = 1 moves the row's slot 0 to a new number. The shared row is what changed.

The fix is a comprehension (lesson 4.7), which runs [0] * 3 again for every row. The name _ is a convention for "a name I won't use":

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

In the editor, press Escape then Tab to move on.

It's as if you wrote [0] * 3 three times. Step through and watch three separate rows appear:

Three rows, three objects

Press Next to run one line at a time. Each arrow shows the object a name refers to.

grid = []
grid.append([0] * 3)
grid.append([0] * 3)
grid.append([0] * 3)
grid[0][0] = 1

grid refers to a list, [].

Step 1 of 5

Step 1 of 5. Line 1: grid = []. grid now refers to a new list, [].

Making a real copy

When you do want a second list, ask for one: .copy() or list(...) both make a new list with the same items.

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

In the editor, press Escape then Tab to move on.

But what if the items are lists themselves? Predict:

What will this print?

Decide before you look. Guessing wrong is how this sticks.

data = [[1, 2], 3]
backup = data.copy()
data[0].append(9)
print(backup)
Pick the output

Python prints

[[1, 2, 9], 3]

.copy() makes a new outer list whose slots refer to the same objects. The inner list is shared, so backup sees the 9.

.copy() copies only the outer list: a . For a copy all the way down, use deepcopy from Python's copy module (imported like random in lesson 3.5):

Names and objects

Press Next to run one line at a time. Each arrow shows the object a name refers to.

import copy
data = [[1, 2], 3]
backup = data.copy()
safe = copy.deepcopy(data)
data[0].append(9)

Step 1 of 5

Step 1 of 5. Line 1: import copy. Nothing in memory changed.

backup's slot 0 refers to the same inner list as data's. safe has its own inner list, so the 9 never reaches it. (The 3 can't change, so sharing it is harmless.) Forget the import and Python says what's missing:

The rule

One sentence covers all of it: assignment never copies; names refer to objects, and changing an object is seen through every name that refers to it. An object that can change is : lists, dicts, and sets. Numbers, strings, and tuples are immutable. This rule comes back in Module 5, where a list used as a function's default value surprises people.

Exercise

The backup that keeps changing

This program keeps a backup of the scores before adding a new one, but the backup changes too. Fix it so the backup keeps the old scores.

Your code

scores = [67, 82, 45]
backup = scores
scores.append(90)
print(backup)
print(scores)

In the editor, press Escape then Tab to move on.

Show a solution

This is one way to solve it, not the only one. If yours prints the same thing, it works.

scores = [67, 82, 45]
backup = scores.copy()
scores.append(90)
print(backup)
print(scores)

Key takeaways

  • Assignment never copies: b = a gives the same object a second name.
  • Changing an object (append, += on a list, b[0] = ...) shows through every name; moving a name (b = [9]) doesn't.
  • == compares values; is asks whether two names refer to the same object.
  • [[0] * 3] * 3 repeats one row; build rows with a comprehension instead.
  • .copy() is shallow: inner lists are shared. copy.deepcopy() copies all the way down.