Python Part I · Module 5 · Lists
Two Names, One List: Copies and References
Four chapters of this module have run into the same thing and promised to explain it here. A list was changed through one name and the change appeared under a different name. A list was repeated and every row turned out to be the same row. Adding to a list with plus-equals behaved differently from adding with plus. All three are the same fact wearing different clothes, and the fact is simple once stated: assigning a list to a second name does not make a second list. This chapter states it, shows it, gives you the two ways to make a genuinely independent copy, and is honest about the one thing those copies do not do.
Intermediate16 min readPythonPart IFundamentalsListsReferences
Part I · Module 5 · Chapter 5.8 · Two Names, One List: Copies and References
1. The Surprise
Here is the program that sends people looking for a bug in Python:
a = ["smoke"]
b = a
b.append("reset")
print(a)
print(b)['smoke', 'reset']
['smoke', 'reset']Nothing was appended to a. a appears on line 1 and then only inside print. Yet a has two elements.
This is not a bug, and it is not a special rule about lists. It is Chapter 2.3's model of assignment, applied to a value that can change — and once you see that, it stops being surprising and becomes something you can use deliberately.
2. What b = a Actually Does
Chapter 2.3 established the model and this chapter needs nothing more than it: a name refers to a value. A name does not contain a value, and one name does not contain another.
So b = a does exactly what it did in Module 2. It evaluates a, which produces a list, and makes b refer to that list. It does not make a second list, and it does not connect b to a.
Afterwards there is one list value and two names for it:
Read the arrows carefully: both start at a name and end at the same list. There is no arrow between a and b, because there is no relationship between them. They were made to refer to the same thing at one moment, and after that moment neither knows the other exists.
Now b.append("reset") is a mutating method from Chapter 5.3. It changes the list. There is one list, so there is one change — and it is visible from either name, because both names lead to it.
That the change is visible through either name is worth seeing from both directions:
x = ["a"]
y = x
x.append("b")
y.append("c")
print(x)
print(y)['a', 'b', 'c']
['a', 'b', 'c']Two appends, made through two different names, both landing in one list. Neither name is the "real" one.
3. Rebinding Breaks the Connection
Chapter 5.3 insisted that mutation and rebinding are different operations. This is where that distinction pays:
p = ["smoke"]
q = p
p = ["boot"]
print(p)
print(q)['boot']
['smoke']p = ["boot"] built a new list and pointed p at it. It did not touch the list p used to refer to, so q — which still refers to that original list — is unchanged.
Compare against Section 1, where the operation was b.append(...). Same shape of program, opposite result:
- Mutation (
b.append(x),b[0] = x,b.sort()) changes the list, and every name referring to it sees the change. - Rebinding (
b = [...]) changes what one name means, and no other name is affected.
This is also the complete explanation of Chapter 5.6 Section 5, where m += [3] and m = m + [3] behaved differently. += on a list mutates it, so the alias saw the change. m + [3] builds a new list and m = rebinds, so the alias did not. Two lines that look equivalent, landing on opposite sides of this section.
4. Making an Independent Copy
When you genuinely want a second list, you have to ask for one. There are three ways and they do the same job:
orig = ["smoke", "reset"]
cop = orig.copy()
cop.append("interrupt")
print(orig)
print(cop)['smoke', 'reset']
['smoke', 'reset', 'interrupt']orig is unchanged. There are now two lists, and appending to one does nothing to the other.
The full slice from Chapter 5.2 does the same thing, which is the job that section said would matter here:
orig2 = ["smoke", "reset"]
cop2 = orig2[:]
cop2.append("interrupt")
print(orig2)
print(cop2)['smoke', 'reset']
['smoke', 'reset', 'interrupt']And so does list(), which Chapter 5.9 covers properly:
orig3 = ["smoke"]
cop3 = list(orig3)
cop3.append("x")
print(orig3, cop3)['smoke'] ['smoke', 'x']All three produce the same picture — and it is deliberately a different picture from Figure 1:
Two names, two lists, one arrow each. Compare it against Figure 1 and the difference is the whole point: there, two arrows met; here, they do not.
Equal is not the same
A copy starts out equal to its original, and == cannot tell them apart:
o = ["a", "b"]
c = o.copy()
print(o == c)
c.append("z")
print(o == c)True
False== compares contents, as Chapter 5.1 established, so two separate lists holding the same elements are equal. What makes them different values is that changing one does not change the other — which only becomes visible the moment you change one.
5. What a Copy Does Not Do
Now the boundary, and it is worth being exact because half-understanding this is worse than not knowing it.
copy() makes a new outer list. It does not go down into the elements and duplicate those. For a list of strings or numbers that makes no difference at all — those values cannot be changed anyway, so sharing them is harmless.
For a list whose elements are themselves lists, it matters:
original = [["smoke"], ["reset"]]
copied = original.copy()
copied[0].append("fast")
print(original)
print(copied)[['smoke', 'fast'], ['reset']]
[['smoke', 'fast'], ['reset']]Both changed. The copy gave us a new outer list, but its two elements are the same two inner lists the original holds — so copied[0] and original[0] are two names for one list, and Section 1 applies to them.
A copy of this kind is called a shallow copy: one level deep.
The outer list really is independent, which is the other half of the truth:
original2 = [["smoke"], ["reset"]]
copied2 = original2.copy()
copied2.append(["burst"])
print(len(original2), len(copied2))2 3Adding a row to the copy did not add a row to the original — the outer lists are genuinely separate. And replacing a whole row in the copy is safe for the same reason:
o3 = [["smoke"], ["reset"]]
c3 = o3.copy()
c3[0] = ["boot"]
print(o3)
print(c3)[['smoke'], ['reset']]
[['boot'], ['reset']]c3[0] = ["boot"] changed which list sits at position 0 of the copy's outer list — that is a mutation of the outer list, which is independent. Only reaching into a shared inner list, as copied[0].append(...) did, is visible through both.
Python does have a way to copy all the way down, and it is not in this module — you do not need it yet, and reaching for it by default is its own mistake.
6. Closing Chapter 5.6's Trap
With Section 1 in hand, the repetition trap explains itself:
rows = [[0] * 3] * 2
rows[0][0] = 9
print(rows)[[9, 0, 0], [9, 0, 0]][0] * 3 was evaluated once, producing one inner list. Then * 2 built an outer list with two elements — and both of those elements are that same one inner list. rows[0] and rows[1] are two names for one list, exactly like a and b in Figure 1, so changing it through one position shows through the other.
Write the rows out and [0] * 3 is evaluated twice, producing two separate lists:
safe = [[0] * 3, [0] * 3]
safe[0][0] = 9
print(safe)[[9, 0, 0], [0, 0, 0]]The inner * was never the problem. It repeats numbers, and a number cannot be changed, so sharing one is invisible. It is repeating a mutable value that causes this.
7. When Sharing Is What You Want
It would be easy to leave this chapter thinking aliases are a hazard to be avoided. They are not — they are how mutation is useful at all.
When a script builds a list of failures and a later part of the script reports them, both parts refer to the same list on purpose. If passing the list around made copies, the additions would go into a copy and the report would be empty. Chapter 5.3's accumulation pattern depends entirely on there being one list that everyone sees.
8. Predict, Run, Observe
Predict every line before reading on. This is the exercise the chapter exists for.
a = ["smoke", "reset"]
b = a
c = a.copy()
b.append("burst")
print(a)
print(b)
print(c)
a = ["boot"]
print(b)
print(a == b)
print(b == c)The verified output:
['smoke', 'reset', 'burst']
['smoke', 'reset', 'burst']
['smoke', 'reset']
['smoke', 'reset', 'burst']
False
FalseWork through it. b = a gives a second name to one list; c = a.copy() makes a second list. So b.append("burst") changes the list a and b share — the first two lines are identical — and leaves c alone at two elements.
Then a = ["boot"] rebinds a. It does not touch the list, so b still shows three elements. That is Section 3.
a == b is False because a now refers to ["boot"]. And b == c is False because b gained an element that c never received.
If you predicted c would have three elements, you read copy() as making another name. If you predicted b would become ['boot'], you read rebinding as mutation.
9. Exercises
Exercise 1 — the canonical case. Before running it, predict tests and backup, explain in two sentences why, and then change one line so that backup is genuinely independent:
tests = ["smoke", "reset"]
backup = tests
backup.append("interrupt")
print(tests)
print(backup)Exercise 2 — one list or two. For each pair of lines, say whether there is one list or two afterwards:
(a) b = a (b) b = a.copy() (c) b = a[:] (d) b = a + [] (e) b = list(a) (f) b = a; b = a.copy()
Exercise 3 — mutation or rebinding. Given x = [1] and y = x, say what y contains after each, and which operation it was:
(a) x.append(2) (b) x = [1, 2] (c) x[0] = 9 (d) x += [2] (e) x = x + [2]
Two of those differ only by a character and give different answers — say which.
Exercise 4 — equal but not the same. Write four lines that produce two lists which are == equal but independent, then prove they are independent in one more line.
Exercise 5 — the shallow boundary. Predict both prints, verify, then explain the difference in three sentences:
a = [["x"], ["y"]]
b = a.copy()
b.append(["z"])
print(len(a))
b[0].append("w")
print(a)Exercise 6 — the grid. Explain in three sentences why [[0] * 2] * 2 produces a structure where changing one value changes two, referring to Figure 1. Then write the version that does not.
Exercise 7 — when not to copy. Describe a situation where taking a copy would be a bug rather than a safeguard, using the failure-accumulation pattern from Chapter 5.3.
Exercise 8 — read the model. A colleague explains the surprise by saying "b points to a". Say why that description is wrong, what it would predict for a = ["boot"] afterwards, and give the accurate one-sentence description.
10. Summary
b = adoes not copy a list. It gives the same list a second name — one list value, two names.- Names refer to values; a name never refers to another name. Both arrows in Figure 1 end at the list.
- Mutation is visible through every name that refers to the list.
b.append(x)changes whatashows. - Rebinding affects only the name rebound.
a = [...]leaves the old list, and every other name for it, alone. - That is the complete explanation of
m += [3]versusm = m + [3]from Chapter 5.6 — one mutates, the other rebinds. a.copy(),a[:]andlist(a)all make an independent list. Usecopy()for clarity.==compares contents and cannot tell one list from two. Only how the second name was produced decides that.- A copy is shallow: a new outer list, the same elements. Adding, removing and replacing at the outer level are independent; changes made inside a nested element are shared.
- For flat lists of strings or numbers, shallow is a full copy in practice, because those elements cannot change.
[[0] * 3] * 2repeats one inner list twice — the same aliasing, produced by an operator instead of an assignment.- Sharing is often correct. Copy when the original must be preserved, not out of habit.
One chapter remains, on building lists out of values you already have.
Where this fits
Part of the Python curriculum.
