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 copyDeep copy
copy.copy(x), x.copy(), x[:], list(x), dict(x)copy.deepcopy(x)
New outer object, shared inner objectsNew outer and new inner objects
Fast: copies only the top levelSlower: walks the whole structure
Fine for flat lists of numbers/stringsNeeded 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)].