Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a = ["smoke"]
b = a
b.append("reset")
print(a)
print(b)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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:

Two names, a and b, on the upper row. A single list value below and between them, holding the string smoke. An arrow runs from a down to that list, and another arrow runs from b down to the same list. There is no connection between a and b.nameanamebone list value["smoke"]12
Figure 1 — the state after b = a. Both names refer to the same list; neither refers to the other. There is no second list anywhere in this picture, which is why a change made through one name is visible through the other.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x = ["a"]
y = x
x.append("b")
y.append("c")
print(x)
print(y)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
p = ["smoke"]
q = p
p = ["boot"]
print(p)
print(q)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
orig = ["smoke", "reset"]
cop = orig.copy()
cop.append("interrupt")
print(orig)
print(cop)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
orig2 = ["smoke", "reset"]
cop2 = orig2[:]
cop2.append("interrupt")
print(orig2)
print(cop2)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['smoke', 'reset']
['smoke', 'reset', 'interrupt']

And so does list(), which Chapter 5.9 covers properly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
orig3 = ["smoke"]
cop3 = list(orig3)
cop3.append("x")
print(orig3, cop3)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['smoke'] ['smoke', 'x']

All three produce the same picture — and it is deliberately a different picture from Figure 1:

Two names, a and b, on the upper row. Below each is its own separate list value, and each list holds the string smoke. An arrow runs from a down to the first list, and a separate arrow runs from b down to the second list. The two lists are distinct.nameanamebfirst list["smoke"]second list["smoke"]12
Figure 2 — the state after b = a.copy(). Two separate list values now exist, each with its own name. The lists hold equal contents at this moment, but they are different values: a change made to one cannot appear in the other.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
o = ["a", "b"]
c = o.copy()
print(o == c)
c.append("z")
print(o == c)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
original = [["smoke"], ["reset"]]
copied = original.copy()
copied[0].append("fast")
print(original)
print(copied)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
original2 = [["smoke"], ["reset"]]
copied2 = original2.copy()
copied2.append(["burst"])
print(len(original2), len(copied2))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2 3

Adding 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
o3 = [["smoke"], ["reset"]]
c3 = o3.copy()
c3[0] = ["boot"]
print(o3)
print(c3)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[['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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rows = [[0] * 3] * 2
rows[0][0] = 9
print(rows)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[[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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
safe = [[0] * 3, [0] * 3]
safe[0][0] = 9
print(safe)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[[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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
['smoke', 'reset', 'burst']
['smoke', 'reset', 'burst']
['smoke', 'reset']
['smoke', 'reset', 'burst']
False
False

Work 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 = a does 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 what a shows.
  • 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] versus m = m + [3] from Chapter 5.6 — one mutates, the other rebinds.
  • a.copy(), a[:] and list(a) all make an independent list. Use copy() 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] * 2 repeats 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.