LESSON 48 · DEEPER PYTHON30 min

Shallow vs Deep Copy in Python

Identify shared nested references and choose a copy depth that protects the original data.

Before you start: Lists · Dictionaries · Functions

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Shallow vs Deep Copy in Python

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    We often write a function that returns an updated copy of a record because the caller still needs the original. In this example, a report is a dictionary containing a list of score dictionaries. The function calls report.copy, changes one score in the result, and returns it. That sounds safe until we inspect the original report and find that its score changed as well. The problem is not the assignment syntax for the score. It is the depth of the copy. We will reproduce the mutation, inspect object identity at the outer and inner levels, and choose a copy strategy that matches the function's promise. Then we will test an unmodified sibling row, because a partial repair can pass the obvious updated-score check while still sharing other mutable data.

  2. 00:49

    Open the companion lesson later to run the same examples and complete its checked non-mutating update task. The updated report has ten points in its first row, as requested. The original now has ten too, which violates the promise to leave it unchanged. Dictionary copy creates a new outer dictionary, but it reuses references to the values already inside it. The value under scores is one list object. Both dictionaries point to it, and that list points to the same row dictionaries. Changing a row through updated therefore changes the row visible through original. The word copy is not a guarantee of complete independence.

  3. 01:30

    It is useful to ask exactly which layer was copied. A shallow copy may be perfectly appropriate if we only add a new top-level key. For an edit inside nested mutable data, it does not protect the source. We should confirm that diagnosis with identity checks rather than relying only on equal printed contents. The first comparison is false, so the outer list is new. The second is true, so both outer lists refer to the same first inner list. After we append nine through second, first shows the change. Equality would not answer this question: two independent lists can hold equal contents.

  4. 02:10

    Identity tests whether two references point to one object. Python assignment makes another reference without copying; list.copy or dict.copy makes a shallow outer copy; copy.deepcopy recursively copies nested data in this ordinary list-and-dictionary structure. Deep copying has a cost and is not always the right design. If you know exactly which branch must change, you could copy only that branch and its ancestors. Our exercise has a stronger contract: every nested score row in the returned report should be independent of the source, including rows we did not initially update. That makes deepcopy a clear teaching solution.

  5. 02:52

    Now the returned report has the new first score, while the original keeps four and seven. The copy happens before the mutation, and the mutation targets only the copy. For this simple structure, deepcopy handles the nested list and row dictionaries. It does not mean every object in every application should be deep-copied. Large object graphs can cost time and memory, and some special objects define their own copy behavior. Prefer to state an observable contract and choose the least complicated method that satisfies it. In the website task, the prompt says the returned report must be independent at every nested score row.

  6. 03:33

    That lets the checker distinguish a complete solution from a shallow copy that merely looks correct in one printout. Also notice that the function returns the updated report rather than printing it internally. The caller gets to decide how to display or store both versions. The first two assertions test the obvious requirement: updated first score, unchanged original first score. The next assertion is more revealing. We alter the second row in the returned report after the function has finished. If that row were still shared, the source's second score would become ninety-nine and the test would fail. The final identity check confirms the two row dictionaries are separate objects.

  7. 04:17

    These checks protect the function's behavior even if someone later changes its implementation from deepcopy to explicit branch copies. Think about what would happen if the report also held an audit list. A copy that isolates scores but shares audit entries might still surprise a caller who appends an event to the returned report. That is why the intended mutation boundary should be stated before implementing a copy. Full independence is simple to describe but can be expensive for a large graph; branch-level copying is faster only when you know every mutable path that needs protection. In your own code, decide whether you need a new outer container, a new nested branch, or a fully independent structure.

  8. 05:03

    Then test mutation at the deepest level you promise to isolate. A quick printed equality comparison is not enough. The main habit is to trace references, not just values: which names and containers still point at the same mutable object? Once you can answer that, shallow and deep copy stop being mysterious vocabulary.

Understand the concept

Assignment does not copy an object. If second = first, both names refer to the same list, so changing it through either name is visible through both. A shallow copy such as first.copy() creates a new outer list, but its existing elements are reused. For immutable numbers and strings that sharing is usually harmless. For a nested list or dictionary it can be surprising, because both outer lists still contain a reference to the same inner object.

The copy module's deepcopy function recursively copies nested mutable containers while keeping the relationship between repeated references in the copied graph. Use it when you need an independent nested structure. A deep copy is not automatically the best design: it costs work and memory, and real objects may define custom copy behavior. When only one known branch needs changing, you can also copy just the outer container and that branch explicitly.

Compare identities at the level you intend to change. A shallow copy is not the same object as its source, but source[0] is shallow[0] can still be true. Equality does not reveal that difference: two independent lists can have equal contents. The useful question is whether a mutation in the result is allowed to affect the source.

In the checked task, a report is a dictionary with a name and a list of score dictionaries. Implement with_updated_score(report, index, score) to return an updated independent report. Replacing one score in the result must leave the original report unchanged. The checker also mutates an unmodified nested row in the returned report, revealing a shallow copy that looked correct after the first update.

  • Assignment shares one object
  • A shallow copy only duplicates the outer container
  • deepcopy recursively duplicates nested mutable objects
  • Equality and identity answer different questions

See it step by step

Read the code, predict the output, then compare it with the result.

01. Assignment shares the same list

PYTHON
first = [[1], [2]]
second = first
second[0].append(9)
print(first)
print(second is first)
EXPECTED OUTPUT
[[1, 9], [2]]
True

Both names refer to exactly the same outer list, so the edit is visible from first.

02. A shallow copy shares inner lists

PYTHON
first = [[1], [2]]
second = first.copy()
second[0].append(9)
print(first)
print(second is first)
print(second[0] is first[0])
EXPECTED OUTPUT
[[1, 9], [2]]
False
True

The outer list is new, but the first inner list remains shared.

03. Deep copy isolates nested data

PYTHON
from copy import deepcopy
first = [[1], [2]]
second = deepcopy(first)
second[0].append(9)
print(first)
print(second)
print(second[0] is first[0])
EXPECTED OUTPUT
[[1], [2]]
[[1, 9], [2]]
False

The nested list is independent, so modifying the result does not alter the source.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Assignment shares an object

A Python variable holds a reference to an object. Assigning second = first makes a second reference to the same list; it does not duplicate the list. An in-place change through second appears through first because there is only one object. This is especially visible with nested lists, where a small append reaches an inner mutable object.

The identity operator is answers whether two references point to the same object. It answers a different question from equality, which compares contents. Two lists can be equal without being identical. Before choosing a copying tool, identify which objects may be mutated and whether any other part of your program still depends on them.

PYTHON
first = [[1], [2]]
second = first
second[0].append(9)
print(first)
print(second is first)
EXPECTED OUTPUT
[[1, 9], [2]]
True
Follow the reasoning
  1. The two names refer to one outer list.
  2. Appending through second changes the inner list visible through first.
  3. Identity confirms that even the outer object was never copied.
02 / 03

A shallow copy stops at the outer layer

Calling list.copy creates a new outer list but places references to the same existing elements inside it. For numbers and strings this usually causes no visible trouble because those objects cannot be changed in place. For an inner list, both outer lists can still point at one mutable object. The example changes an inner list through the copy and shows the original changing too.

Check identity at the level where the mutation happens. The outer lists are different objects, but their first elements are the same inner list. A shallow copy is appropriate when you only need to add or remove top-level entries and will not mutate shared nested data. It is insufficient for the report task, where the score dictionaries themselves must be independent.

PYTHON
first = [[1], [2]]
second = first.copy()
second[0].append(9)
print(first)
print(second is first)
print(second[0] is first[0])
EXPECTED OUTPUT
[[1, 9], [2]]
False
True
Follow the reasoning
  1. The outer copy passes the not-identical check.
  2. The first inner list is still shared.
  3. The mutation reaches the original through that shared inner reference.
03 / 03

Copy nested report data before editing

The copy module provides deepcopy for an independent nested copy in this simple dictionary-and-list example. It recursively copies the score list and each row dictionary before we edit one row. The original remains available for comparison or for a second operation. Deep copying costs additional work and memory, so use it when the data contract requires this level of separation.

The checked task does more than compare the first edited row. It changes an untouched row in the returned report and confirms that the source row remains unchanged. This catches a half repair that copied only the target row while leaving other mutable parts shared. An alternative implementation could copy each known mutable branch explicitly, but it must satisfy the same observable independence contract.

PYTHON
from copy import deepcopy

def with_updated_score(report, index, score):
    updated = deepcopy(report)
    updated['scores'][index]['points'] = score
    return updated

original = {'name': 'Ada', 'scores': [{'points': 4}, {'points': 7}]}
changed = with_updated_score(original, 0, 10)
print(changed['scores'])
print(original['scores'])
EXPECTED OUTPUT
[{'points': 10}, {'points': 7}]
[{'points': 4}, {'points': 7}]
Follow the reasoning
  1. Deepcopy creates independent nested rows.
  2. Only the returned report receives the replacement score.
  3. The input still has both original score values.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnRepair with_updated_score(report, index, score). Return a report with that score changed while leaving every nested part of the input independent of the result. Preserve other fields and do not mutate report. The prepared output must be 10 then 4.
Need a hint?

Import copy and start with updated = copy.deepcopy(report), then assign updated['scores'][index]['points'] = score. A top-level dict.copy() still shares the scores list.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.After b = a for a list, what is b is a?

    02.What does list.copy() duplicate?

    03.Which check reveals whether two nested rows are shared?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Mistaking an outer copy for full isolation

    COMMON MISTAKE
    updated = report.copy()
    updated['scores'][0]['points'] = 10
    FIX
    from copy import deepcopy
    updated = deepcopy(report)
    updated['scores'][0]['points'] = 10

    What happens: The input's nested score changes too.
    A shallow copy retains the original scores list and row dictionaries.

    Using equality to prove independence

    COMMON MISTAKE
    assert copied == original  # therefore no shared objects
    FIX
    assert copied == original
    assert copied is not original
    assert copied['scores'][0] is not original['scores'][0]

    What happens: Equal contents do not show whether nested objects are shared.
    Inspect identity at the mutable level whose independence matters.

    Browse this lesson’s problem fixes

    Sources and further reading