Shallow vs Deep Copying

Why a copy of a list of lists still shares its inner lists, and when you need copy.deepcopy.

Overview

What a shallow copy actually does

original = [[1, 2], [3, 4]]
shallow = original[:]
shallow[0][0] = 99
print(original)  # [[99, 2], [3, 4]]

The outer list is new — original is shallow is False, and appending to one does not affect the other. But the two inner lists were not copied; both outer lists point at the same two inner lists. Change something one level down and both see it.

original[0] is shallow[0] is True, which is the whole story in one line.

shallow.py

shallow.py Python 3
Output

                    

copy_when.py

copy_when.py Python 3
Output

                    

Worth knowing

A shallow copy duplicates the outer container and reuses everything inside it.
[:], list(x) and x.copy() are all shallow.
Nested mutable data is when you need copy.deepcopy.
If everything inside is immutable, shallow is enough - there is nothing to share.

Shallow vs Deep Copying: A Practical Guide

Copying a list gives you a new list. It does not give you new copies of the things inside it, and that distinction is where the bugs live.

The three shallow copies

nums[:]    list(nums)    nums.copy()

All equivalent. dict.copy() and set.copy() behave the same way, and dict(d) is the dict equivalent of list(l).

When shallow is enough

If everything inside is immutable — numbers, strings, tuples of those — a shallow copy is a complete copy in every way that matters. There is nothing shared that can change, so the distinction disappears.

That covers most everyday copying, which is why [:] is so common and why the problem stays hidden until the day your data has a list inside a list.

When you need deep

import copy
deep = copy.deepcopy(original)

deepcopy walks the whole structure and rebuilds every mutable object it finds. Nested config dictionaries, lists of records, anything parsed from JSON — these are the cases.

It is slower, and for large structures noticeably so. It also handles the hard cases correctly: shared references stay shared in the copy, and cycles do not cause infinite recursion. Writing your own recursive copy usually gets both of those wrong.

The dict version of the trap

config = {"limits": {"max": 10}}
shallow = config.copy()
shallow["limits"]["max"] = 999

The original now reads 999 too. This is the same rule and it bites harder with configuration, because the nesting is the point of the structure.

The rule

Ask what is inside. Flat and immutable: use a slice or .copy(). Nested and mutable: use deepcopy, or restructure so you are not copying a mutable tree at all.

Check yourself

0 of 3

Answer without scrolling back up.

  1. After a shallow copy of `[[1, 2]]`, changing `copy[0][0]`:

  2. When is a shallow copy sufficient?

  3. Which of these is NOT a shallow copy of a list?

Cheat sheet

Shallow vs Deep Copying

The outer list is new — original is shallow is False, and appending to one does not affect the other. But the two inner lists were not copied; both outer lists point at the same two inner lists. Change something one level down and both see it.

PYTHON · vizlearn.in/python/shallow_and_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.