Why this matters
This lesson explains the bug behind countless confused forum posts: "I changed one list and another one changed too!" It happens because Python names are references to objects, and some objects (lists, dicts, sets) can change in place. Once you see what is really going on in memory, the behavior becomes predictable, and you will know exactly when to copy, how deep to copy, and how to write functions that do not surprise their callers.
The model: names refer to objects
From the start of this track you have seen that assignment binds a name to an object. It never copies the object:
a = [1, 2, 3]
b = a
a ---+
+---> [1, 2, 3] one list object
b ---+
a and b are two names for the same list. This is called aliasing. You can check it with is, which compares identity (same object), versus ==, which compares value (equal contents):
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == b, a is b)
print(a == c, a is c)
True True
True False
c is a different list that happens to have equal contents.
Mutation versus rebinding
Two very different things can happen to a from here:
Mutating the object changes it in place. Every name referring to it sees the change:
a = [1, 2, 3]
b = a
a.append(4) # mutate
a[0] = 100 # mutate
print(b)
[100, 2, 3, 4]
Rebinding a name points it at a different object. Other names are unaffected:
a = [1, 2, 3]
b = a
a = [7, 8] # rebind a to a new list
print(b)
[1, 2, 3]
Ask yourself of every line: does it change an object (method like append, sort, item assignment x[i] = ..., del x[i], += on a list), or does it make a name refer to something else (plain name = ...)?
Mutable and immutable types
| Immutable (cannot change after creation) | Mutable (can change in place) |
|---|---|
int, float, bool, None | list |
str | dict |
tuple, frozenset | set |
With immutable objects, aliasing is never a problem, because nobody can change the shared object. Every "change" makes a new one:
s = "hi"
t = s
s += "!" # builds a new string, rebinds s
print(t) # hi
This is why += behaves differently by type. On a list, x += [3] calls x.extend([3]), mutating in place, so aliases see it. On a tuple or string, += must create a new object and rebind the name.
Aliasing in disguise
Aliasing does not only come from b = a. Any time a reference is stored, you get another way to reach the same object.
Containers hold references. Putting a list into another list stores a reference:
row = [0, 0]
grid = [row, row]
grid[0][0] = 5
print(grid)
[[5, 0], [5, 0]]
List multiplication repeats references. This is the famous grid trap:
grid = [[0] * 3] * 3
grid[1][1] = 1
print(grid)
[[0, 1, 0], [0, 1, 0], [0, 1, 0]]
grid ---> [ ref, ref, ref ]
\ | /
v v v
[0, 1, 0] only one row list exists
[0] * 3 is fine, because the repeated item 0 is an immutable int. Repeating a mutable object gives several references to that single object. Build rows in a loop so each iteration creates a fresh list:
grid = []
for _ in range(3):
grid.append([0] * 3)
(_ is a conventional name for a loop variable you do not use.)
Function arguments are aliases. Calling a function binds its parameter to the caller's object, with no copy. So a function can mutate the caller's list:
def add_bonus(scores):
for i in range(len(scores)):
scores[i] += 10
mine = [50, 60]
add_bonus(mine)
print(mine)
[60, 70]
Sometimes that is the point (list.sort() works this way and returns None to signal it). Often it is an accident. Rebinding the parameter, on the other hand, never affects the caller:
def reset(scores):
scores = [] # rebinds the local name only
mine = [50, 60]
reset(mine)
print(mine) # [50, 60]
People sometimes ask whether Python is "pass by value" or "pass by reference". The accurate answer is that Python passes references to objects (sometimes called "pass by object reference" or "call by sharing"): the function can mutate a mutable object it receives, but cannot make the caller's name refer to something else.
A sensible rule: functions should not mutate their arguments unless that is their documented purpose. Return a new object instead.
Copying
When you do want an independent object, copy it explicitly.
Shallow copies create a new outer container holding the same element references:
a = [1, 2, 3]
b = a.copy() # also: list(a), a[:]
b.append(4)
print(a, b) # [1, 2, 3] [1, 2, 3, 4]
Dicts and sets have .copy() too, and dict(d) and set(s) also copy.
A shallow copy is enough when the elements are immutable. With nested mutable elements, the inner objects are still shared:
outer = [[1, 2], [3]]
shallow = outer.copy()
shallow.append([4]) # only shallow changes: outer lists differ
shallow[0].append(99) # inner list is shared!
print(outer)
print(shallow)
[[1, 2, 99], [3]]
[[1, 2, 99], [3], [4]]
outer ---> [ ref, ref ]
| \
v v
[1,2,99] [3] inner lists shared
^ ^
shallow ---> [ ref, ref, ref ]
\
v
[4]
Deep copies recursively copy everything reachable, giving a fully independent structure:
import copy
outer = [[1, 2], [3]]
deep = copy.deepcopy(outer)
deep[0].append(99)
print(outer, deep)
[[1, 2], [3]] [[1, 2, 99], [3]]
deepcopy is slower and uses more memory, and it copies everything, which is not always what you want (for example, objects that represent an open file or a shared configuration). Use it when you need a truly independent snapshot of nested data.
Where the default-argument trap fits
Recall the mutable default from the parameters lesson: def add_item(item, basket=[]). Now you can describe it precisely: the default list is one object, created once at def time, and every call that omits basket gets an alias to that same object. Mutating it mutates the shared default.
Common mistakes
- Thinking
b = acopies a list. It aliases. Usea.copy()(orcopy.deepcopyfor nested data). [[...]] * nfor grids: n references to one row. Build rows in a loop.- Mutating an argument by accident, surprising the caller. Build and return a new list.
- Shallow-copying nested data and expecting independence.
- Using
isto compare values:ischecks identity. Use==for equality, and keepisforNone(x is None).
Recap
- Names are references; assignment never copies. Two names for one object are aliases.
- Mutation changes the object (all aliases see it); rebinding moves one name.
- Lists, dicts and sets are mutable; numbers, strings, tuples and frozensets are not.
- Containers,
*repetition and function arguments all create aliases. .copy(),list(x),x[:]make shallow copies;copy.deepcopycopies nested structures.
Going further: paste the examples from this lesson into Python Tutor (pythontutor.com), which draws names and objects as boxes and arrows. Then read the documentation of the copy module, including how classes can customize copying with __copy__ and __deepcopy__.