Lists and names: the aliasing trap

Part of the free LogicWiz Python course, Module 5: Lists and tuples. The examples and practice run in the app.

Meher wrote tomorrow = today to start tomorrow's order list, then cleared tomorrow. Today's orders vanished too. She never touched today. What happened?

You'll learn to

  • See a name as a label pointing to an object, not a box
  • Tell changing a list apart from pointing a name at a new list
  • Copy a list properly, and build a grid without the `[[0]*3]*3` trap

Learn

Names are labels

Think of a name as a sticky label on an object, not a box that holds it.

today = ["Chai", "Samosa"] makes one list and sticks the label today on it.

tomorrow = today does not copy the list. It sticks a second label on the same list. Two names, one list. This is called aliasing.

So anything you do to the list through tomorrow (append, remove, change an item) shows up through today too, because there is only one list.

Two names, one list

today = ["Chai", "Samosa"]
tomorrow = today
tomorrow.append("Maggi")
print(today)
print(tomorrow is today)

is asks "are these the same object?" Step through: after line 2, both names point at one list.

Changing a list vs moving a label

Two very different things look alike:

  • Changing the list (mutating): b.append(3), b[0] = 9, b.remove(x), b += [3]. Every name on that list sees it.
  • Moving the label (rebinding): b = b + [3] or b = [9]. The right side builds a new list and b now points there. The old list, and any other name on it, is untouched.

The surprise: b += [3] changes the list in place, but b = b + [3] makes a new one.

Rebinding leaves the other name alone

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

c = a
c += [4]
print(a, c)

Making a real copy

a = [10, 20, 30]
b = a[:]
c = list(a)
d = a.copy()
b[0] = 99
print(a, b)
print(a == c, a is c)

import copy
marks = [[90, 80], [70, 60]]
shallow = marks.copy()
deep = copy.deepcopy(marks)
marks[0][0] = 0
print(shallow)
print(deep)

a[:], list(a) and a.copy() make a new outer list, but the items inside are still shared. For a list of lists, only copy.deepcopy copies the inner lists too.

The [[0]*3]*3 trap

grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)

safe = []
for i in range(3):
    safe.append([0] * 3)
safe[0][0] = 1
print(safe)

[row] * 3 repeats the label, not the row: all three rows are one list. The loop makes a fresh [0, 0, 0] each time.

A function gets the same list you pass in, not a copy. If def add(lst): lst.append(5) runs on your list, your list changes. If the function should not touch the caller's list, copy it first: lst = lst[:].

Board papers love "predict the output" with b = a followed by b.append(...). Interviews ask "what is the difference between a shallow and a deep copy?" Answer: a shallow copy makes a new outer list that shares the inner objects; a deep copy copies everything, all the way down.

In one line

b = a gives one list two names; copy with a[:], list(a) or a.copy() (and copy.deepcopy for nested lists); build grids with a loop, never [[0]*n]*n.