Copies: assignment, shallow and deep
Part of the free LogicWiz Python course, Module 13: Python under the hood. The examples and practice run in the app.
Meher made tomorrow's menu with tomorrow = today.copy() and added poha to tomorrow's snacks. Next morning, today's printed menu also had poha. She did make a copy. So what happened?
You'll learn to
- Tell assignment, shallow copy and deep copy apart on nested data
- Use `copy.copy` and `copy.deepcopy` and know which one you need
- Avoid the `[[0] * 3] * 3` grid trap
- Protect a caller's data inside a function
Learn
Three ways to "copy"
- Assignment
b = a: no copy at all. A second label on the same object. - Shallow copy
copy.copy(a): a new outer object, but the items inside are the same objects as before. - Deep copy
copy.deepcopy(a): a new outer object and new copies of everything inside, all the way down.
For a flat list of numbers or strings, shallow is enough, because the items are immutable. The difference only bites when there are mutable things inside (lists in a list, lists in a dict).
One change, three results
import copy
today = {"snacks": ["samosa", "vada"], "drinks": ["chai"]}
alias = today
shallow = copy.copy(today)
deep = copy.deepcopy(today)
today["snacks"].append("poha")
print(alias["snacks"])
print(shallow["snacks"])
print(deep["snacks"])
shallow is a new dict, but its "snacks" value is the same list as today's. Only deep got its own list.
Shallow vs deep
| Shallow copy | Deep copy |
|---|---|
| copy.copy(x), x.copy(), x[:], list(x), dict(x) | copy.deepcopy(x) |
| New outer object, shared inner objects | New outer and new inner objects |
| Fast: copies only the top level | Slower: walks the whole structure |
| Fine for flat lists of numbers/strings | Needed for nested mutable data |
Changing vs replacing an inner item
import copy
marks = [[90, 80], [70, 60]]
backup = copy.copy(marks)
marks[0].append(100)
marks[1] = [0, 0]
print(backup)
marks[0].append changes a list both share, so backup sees it. marks[1] = [0, 0] only replaces a slot in the outer list of marks. The backup's outer list is separate, so it keeps the old [70, 60].
The grid trap
bad = [[0] * 3] * 3
bad[0][0] = 1
print(bad)
good = [[0] * 3 for _ in range(3)]
good[0][0] = 1
print(good)
* 3 on the outer list copies the reference three times: three labels on one row. The comprehension runs [0] * 3 three times and builds three rows.
list.copy(), dict.copy(), slicing [:], list(x) and dict(x) are all shallow. If the data has lists or dicts inside, they still share those inner objects. When in doubt with nested data, use copy.deepcopy.
Copies and functions. A function gets the caller's object, not a copy. If it must change the data without surprising the caller, copy first: items = list(items) for a flat list, data = copy.deepcopy(data) for nested data. deepcopy even handles objects that point to themselves (cycles) without looping forever.
In one line
b = a copies nothing; copy.copy/.copy()/[:] copy only the outer object; copy.deepcopy copies everything inside too.
Interview corner
What is the difference between a shallow copy and a deep copy?
A shallow copy (copy.copy, list.copy(), slicing) creates a new outer container but fills it with references to the same inner objects. A deep copy (copy.deepcopy) recursively copies the inner objects too, so the two structures share nothing mutable. They only behave differently when the container holds mutable objects like lists or dicts.
Name four ways to copy a list. Are they deep or shallow?
a[:], list(a), a.copy() and copy.copy(a), and also [*a] and [x for x in a]. All of them are shallow. Only copy.deepcopy(a) is deep.
Why does `[[0] * 3] * 3` behave strangely?
The outer * 3 repeats a reference to the same inner list three times, so changing grid[0][0] changes every row. Build rows separately: [[0] * 3 for _ in range(3)].