What's the difference between a shallow copy and a deep copy?

6 minintermediatecollectionscopyshallow-copydeep-copy

Quick Answer

A **shallow copy** (`list(x)`, `x.copy()`, `copy.copy(x)`) creates a new outer container but reuses references to the *same* nested/inner objects — mutating a nested object through the copy affects the original too. A **deep copy** (`copy.deepcopy(x)`) recursively copies every nested object as well, so the copy is fully independent of the original, at the cost of more time and memory.

Detailed Answer

Shallow copy: new outer container, shared inner objects

import copy

original = [[1, 2], [3, 4]]
shallow = copy.copy(original)          # or: original[:] / list(original)

shallow.append([5, 6])                  # doesn't affect original -- outer list is new
original                                  # [[1, 2], [3, 4]]

shallow[0].append(99)                    # mutates the SHARED inner list!
original                                  # [[1, 2, 99], [3, 4]]  -- original changed too!

shallow is a genuinely new list object, but its elements are the same inner list objects as original's — appending to the outer copy doesn't touch the original, but mutating a shared inner list does, since both original[0] and shallow[0] point at the identical object.

Deep copy: fully independent

deep = copy.deepcopy(original)
deep[0].append(100)
original   # unaffected -- deep copy recursively copied every nested list too

copy.deepcopy recursively copies every object reachable from the top, building an entirely independent structure — safe to mutate at any depth without affecting the original, at the cost of recursively copying (and therefore more time/memory, and needing to handle cycles, which deepcopy does via a memo dict to avoid infinite recursion).

Which one for which types

d = {"a": [1, 2]}
d.copy()          # shallow -- new dict, same inner list object
copy.deepcopy(d)   # deep -- new dict AND a new inner list

t = (1, [2, 3])
copy.copy(t)        # shallow -- new tuple, same inner list

Most built-in containers offer a .copy() method (or slicing [:]) that performs a shallow copy; there's no built-in shortcut for a deep copy — copy.deepcopy is always the tool for that.

When shallow is fine, and when it isn't

Shallow copy is fine (and cheaper) when the container only holds immutable elements (int, str, tuple of immutables) — there's no "shared inner object" risk because nothing can mutate them in place. It becomes a real bug source specifically when elements are themselves mutable (nested lists/dicts/objects) and you need the copy to be fully independent.

Interview-ready summary: Shallow copy duplicates only the top-level container; nested mutable objects are still shared with the original. Deep copy recursively duplicates everything reachable, giving full independence at higher cost. Reach for shallow copy (or plain slicing) when contents are immutable or sharing is intentional; reach for deepcopy when you need a completely independent structure.

Related Resources