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.
Every other spelling of copy is the shallow one, for dicts as well as lists.
And the two things a naive recursive copy would get wrong.
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.