Shallow copy vs deep copy

A shallow copy builds a new container holding the same objects. A deep copy rebuilds the objects too, recursively. Slicing, list(), dict.copy() and copy.copy() are all shallow — so mutating a nested value through one name is visible through the other.

Python semanticsConceptualMedium

Step through it

What to watch

  • Both copies are new outer objects — that part is not the difference.
  • The shallow copy shares the inner lists, which is where the mutation leaks.
  • original[:] and list(original) behave exactly like copy.copy.

Say this out loud

"A shallow copy copies the outer container and shares the contents, so nested mutation leaks between them. copy.deepcopy rebuilds the whole tree. Slicing and list() are shallow, which is fine for flat data and a bug for nested data."

Overview

A shallow copy creates a new outer container but keeps references to nested objects. A deep copy recursively duplicates nested objects; choose it only when independent nested state is required.

Run it

The divergence at the inner level, the leak demonstrated by one append. And the cycle deepcopy survives.

Where the two diverge, and the one append that proves it.

1Python
Output

Every other spelling of copy is the shallow one, for dicts as well as lists.

2Python
Output

And the two things a naive recursive copy would get wrong.

3Python
Output

What "shallow" means exactly

A shallow copy allocates a new container and fills it with the same references the first holds. So the outer objects are free — appending to the copy does not lengthen the first — and every element is shared.

For flat data that distinction never appears. A list of integers or strings copies shallowly and behaves exactly as you would want because the shared elements are immutable and nobody can change them. The bug needs two ingredients: nesting, and mutation.

Everything is shallow unless you asked

These are all the same operation: b = a[:], b = list(a), b = a.copy(), b = copy.copy(a), b = [*a], and dict(d) for dictionaries. One level.

copy.deepcopy is the only one in that list that recurses. It also handles the two things a naive recursion would get wrong: cycles, via a memo of objects already copied. So a list containing itself does not hang; and identity sharing. So if the same object appears twice in the source it appears twice as the same object in the result rather than being duplicated.

What deepcopy costs, and when to avoid it

It walks the entire object graph and rebuilds it, so it is proportional to the total size rather than the top-level length. And it is slow enough to notice in a loop. It also copies things you may not want copied: a nested object holding a database connection, an open file or a lock either fails or produces a duplicate that is meaningless.

Classes control this. __deepcopy__ and __copy__ let a type define what copying means, and copy.deepcopy falls back to the pickle protocol (__reduce_ex__) — which is why objects that cannot be pickled often cannot be deep-copied either, for the same reason.

The cheapest fix is often neither: restructure so the nested mutable is not shared in the first place, or use immutable values so copying is unneeded.

The follow-up: the nested-list multiplication bug

"What does [[0] * 3] * 3 give you?" A list of three references to one inner list. So setting grid[0][0] sets the first cell of every row. It is the same fact as this page — the outer sequence was copied and the inner one was shared — arriving through multiplication rather than through a copy call. The fix is a comprehension, which evaluates [0] * 3 once per row.

Edge cases to raise

Volunteering these is most of what separates a correct answer from a good one.

Tuples are only as immutable as their contents. ([1, 2], 3) cannot be reassigned and its inner list can still be appended to. So a tuple is not a defence against the problem on this page.

A dict comprehension over a dict is shallow too, as is {**d}. Every spelling of "make me a new dict from this one" shares the values.

copy.deepcopy on a large structure can be slower than recomputing it. Worth saying out loud because it shows you are costing the fix rather than reaching for the safest-sounding call.

Sources and next steps

How the code works

The divergence at the inner level, the leak demonstrated by one append, and the cycle deepcopy survives.

How the code works

  1. copy.copy(original)A new outer list holding the same two inner lists. The outer independence is real and is not what the question is about.
  2. original[0].append(99)One mutation, visible through two names. Nothing was assigned to shallow — it changed because it was never a separate object at that level.
  3. made[0] is original[0]True for slicing, list() and .copy() alike. There is one shallow copy operation with several spellings.
  4. clone[2] is clonedeepcopy kept a memo of what it had already copied, so the self-reference became a reference to the copy rather than an infinite descent.

Change one thing

  • Make the inner values immutable — [(1, 2), (3, 4)] — and try to reproduce the leak. You cannot, which is the argument for immutable values over defensive copying.
  • Put an open file in the structure and call deepcopy. The failure is the same one pickle gives, because deepcopy falls back to the same protocol.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. A shallow copy of [[1,2],[3,4]] gives you:

  2. Which of these is NOT shallow?

  3. Why does deepcopy not hang on a list that contains itself?

  4. When is a shallow copy perfectly safe?

Cheat sheet

Shallow copy vs deep copy

Copy a nested list with list(), copy() and deepcopy(). Mutate an inner list and see which copies notice the change and which stay independent.

INTERVIEW · vizlearn.in/interview/shallow-versus-deep-copy.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.