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.

Overview

The question, and what it is testing

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.

Interviewers use this one because the answer separates people who have read the language from people who have only used it. Nothing here is obscure; all of it is observable, which is what the editor below is for.

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."

Shallow copy vs deep copy

What is the difference between a shallow and a deep copy, and which one does list(x) give you?

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 precisely

A shallow copy allocates a new container and fills it with the same references the original holds. So the outer objects are independent — appending to the copy does not lengthen the original — 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 frequently 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 unnecessary.

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.

What to say 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.

Then stop. The commonest failure on a question like this is answering it correctly in one sentence and then talking for another minute until something wrong comes out.

What to notice while it runs

  • 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.

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.

The follow-ups interviewers ask

"How do you copy something that holds a socket or a lock?" Define __deepcopy__, or __getstate__/__setstate__ to say what should be carried and what should be rebuilt. Otherwise deepcopy falls through to the pickle protocol and fails on exactly the attributes that cannot be serialised.

"Is deepcopy ever the wrong tool even when you need independence?" Often. It is proportional to the whole object graph, so in a loop it dominates. The cheaper answers are to restructure so the nested mutable is not shared, or to use immutable values so no copy is needed - both remove the question rather than answering it.

"What does [[0] * 3] * 3 give you?" Three references to one inner list, so grid[0][0] = 1 sets the first cell of every row. Same fact as this page, reached through multiplication. The fix is a comprehension, which evaluates the inner list once per row.

Common wrong answers

"list(x) gives you an independent copy." One level. Nested mutables are shared, which is the entire question.

"deepcopy is always the safe default." It is slow, it copies things you may not want duplicated, and it fails on unpicklable attributes. Safe is not the same as correct.

"dict.copy() copies the nested dictionaries." It does not. Neither does dict(d) nor a dict comprehension over it.

Recap in one screen

  • 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.
  • The one-line answer: A shallow copy builds a new container holding the same objects.
  • Worth trying: 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.
  • Worth trying: 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.

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

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.

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.