Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Shallow vs. Deep Copies with the `copy` Module

Recommended resources

In our last lesson, we explored Python's cyclic garbage collector, a sophisticated mechanism that relies on a deep understanding of object references to function. We saw how Python tracks relationships between objects to reclaim memory from isolated cycles. Today, we will leverage that same mental model of objects and references to tackle a common and critical task in software development: creating copies.

As a developer, you've likely needed to duplicate a data structure to modify it without affecting the original. However, the distinction between simply assigning a variable and creating a true, independent copy can lead to subtle and frustrating bugs, especially in the complex, stateful backend systems you build. This lesson focuses on mastering Python's copy module to perform shallow and deep copies of nested data structures. We will dissect how each method works at the level of object references, identify the scenarios where one is preferred over the other, and learn how to customize copying behavior in your own classes—a key skill for building robust, predictable systems.

The Foundation: Assignment Creates a Binding, Not a Copy

Before we dive into the copy module, let's solidify a core concept we've touched on previously. In Python, assignment statements do not copy objects; they create new bindings (references) to the same object.

Consider this simple list:

original_list = [1, 2, [3, 4]]
assigned_list = original_list

Here, assigned_list is just another name pointing to the exact same list object in memory as original_list. They are not two separate lists.

>>> assigned_list is original_list
True

Any modification made through one variable will be visible through the other, because they both refer to the single underlying object.

assigned_list[0] = 99
print(original_list)  # Output: [99, 2, [3, 4]]

This behavior is efficient, but it's not what we want when we need to create a distinct duplicate. This is where the copy module becomes essential. The following short video provides an excellent recap of this principle.

Shallow vs. deep copies in Python

This video from Reuven Lerner's channel clearly distinguishes between assignment and copying.

Watch the first minute of the video, from the beginning to the end of the is check. The demonstration using id() and the is operator perfectly illustrates that assignment results in two variables pointing to one object.


Shallow Copy: Duplicating the Top Level

A shallow copy creates a new compound object (like a new list or dictionary) but then populates it with references to the objects found in the original. It copies the object one level deep, but the references to any nested objects remain shared.

You can create a shallow copy in several ways, with copy.copy() being the most explicit:

import copy

original_list = [1, 2, [3, 4]]
shallow_copy_list = copy.copy(original_list)




# The outer list is a new object
print(original_list is shallow_copy_list)         # False




# But the nested list is the SAME object
print(original_list[2] is shallow_copy_list[2])   # True

Because the top-level container is a new object, modifying it won't affect the original.

shallow_copy_list.append(5)
print(original_list)      # [1, 2, [3, 4]]
print(shallow_copy_list)  # [1, 2, [3, 4], 5]

However—and this is the critical part—since the nested list [3, 4] is shared, a change to it through either variable affects both.

shallow_copy_list[2][0] = 'X'
print(original_list)      # [1, 2, ['X', 4]]
print(shallow_copy_list)  # [1, 2, ['X', 4], 5]

This unintended side effect is a classic source of bugs in programs that handle complex mutable state. The diagram below illustrates this concept visually. A change to the nested object (circled) is reflected in both the original SHALLOW structure and its copy.

This diagram shows that in a shallow copy, the top-level list is duplicated, but the nested lists it contains are not. Both the original and the copy hold references to the same inner lists. Consequently, modifying an element within a nested list in one place appears in the other. A deep copy, in contrast, creates fully independent copies of all nested objects.

The following video uses Python Tutor to provide an exceptional visualization of the memory layout during a shallow copy, making the concept of shared internal references crystal clear.

Shallow vs. deep copies in Python

This segment of Reuven Lerner's video demonstrates the various ways to create a shallow copy and then uses a powerful visualization to show exactly why it can be problematic.

Watch from the explanation of shallow copies. Pay close attention to the Python Tutor visualization starting around the visualization part. Observe how the outer lists have different memory addresses, but the inner lists they contain point to the same addresses. This is the key takeaway.


Deep Copy: Creating a Fully Independent Clone

To avoid the side effects of shallow copying, you need a deep copy. A deep copy constructs a new compound object and then, recursively, inserts copies of the objects found in the original. The result is a completely independent clone of the original object and all its children.

The copy module provides copy.deepcopy() for this purpose.

import copy

original_list = [1, 2, [3, 4]]
deep_copy_list = copy.deepcopy(original_list)




# The outer list is a new object
print(original_list is deep_copy_list)         # False




# The nested list is ALSO a new object
print(original_list[2] is deep_copy_list[2])   # False

Now, because every part of the data structure has been duplicated, you can modify any part of the deep_copy_list without any fear of side effects.

deep_copy_list[2][0] = 'Y'
print(original_list)    # [1, 2, [3, 4]]
print(deep_copy_list)   # [1, 2, ['Y', 4]]

For a comprehensive walkthrough of these concepts with a different, practical example, the following article is an excellent resource.

How to Copy Objects in Python: Shallow vs Deep ...

This Real Python article provides a thorough explanation of shallow and deep copying using a nested dictionary example that's highly relevant to the kind of data you might handle in a web backend.

Please read the first three sections: the introduction, Shallow Copy, and Deep Copy. Focus on the "grocery inventory" example to see how these concepts apply to dictionaries. Also, pay attention to the excellent diagrams illustrating the reference graphs. Then, read the section Comparing Shallow and Deep Copying for a summary of when to choose each method.

A particularly interesting challenge for deep copying is handling recursive objects (e.g., a list that contains a reference to itself). A naive recursive copy would enter an infinite loop. The copy.deepcopy() function cleverly avoids this by keeping a memo dictionary of objects it has already copied during a pass, preventing redundant duplication and infinite recursion.

This diagram illustrates how `deepcopy` handles a recursive data structure. The original list, `continued_fraction`, contains a reference to itself. A naive copy would fail, but `deepcopy` creates a new list, `deep_copy`, that also contains a reference to itself, correctly preserving the cyclic structure without entering an infinite loop. The `broken_copy` (a shallow copy) fails to replicate the self-reference correctly.

Customizing Copying Behavior in Classes

As a senior engineer, you don't just use built-in types; you design your own. The default behavior of copy.copy() and copy.deepcopy() on custom objects is to create a new instance and then copy the attributes. For shallow copy, it copies the references to the attributes; for deep copy, it recursively calls deepcopy on the attributes.

This works for simple data-holding classes, but it fails when your object manages an external resource, like a file handle or a network connection, that cannot or should not be copied directly. In these cases, you can take control by implementing special methods.

  • __copy__(self): Defines the behavior for copy.copy().
  • __deepcopy__(self, memo): Defines the behavior for copy.deepcopy(). The memo dictionary must be passed to any internal calls to deepcopy to prevent issues with circular references.

The following reading demonstrates a powerful, practical use case: a class that manages a file. The default copy behavior is incorrect, and the article walks you through implementing __copy__ and __deepcopy__ to provide correct, resource-aware semantics.

How to Copy Objects in Python: Shallow vs Deep ...

This final reading from the Real Python article shows you how to make your own classes work correctly with the copy module, a crucial skill for advanced object-oriented design.

Read the sections titled Customizing Behavior and Managing Resources. The DataFile class is a superb example. Note how __init__ is bypassed during a default copy and why that's a problem for the file handle. Analyze the implementation of __copy__ and __deepcopy__ to see how it correctly creates a new instance and re-initializes the file resource.


Conclusion

In this lesson, we have moved from the abstract world of memory management to the practical application of object and reference manipulation. A solid grasp of copying is non-negotiable for writing correct and predictable Python code, especially when dealing with the complex, mutable state common in production applications.

Here are the essential takeaways:

  • Assignment is not copying: new_var = old_var creates another reference to the same object.
  • Shallow Copy (copy.copy()): Creates a new top-level object but shares references to all nested objects. It's fast but can lead to unintended side effects if nested objects are mutable.
  • Deep Copy (copy.deepcopy()): Creates a fully independent clone by recursively copying every object in the hierarchy. It's safer for nested mutable structures but is slower and uses more memory.
  • Decision Rule: Use a shallow copy when your data structure is flat or contains only immutable objects. Use a deep copy whenever you have nested, mutable objects and need to guarantee that the original and the copy are fully decoupled.
  • Customization: You can and should implement __copy__ and __deepcopy__ for your custom classes when they manage resources or require specific copying logic that differs from the default attribute-by-attribute duplication.

Our exploration of Python's object model continues. In the next lesson, we will examine object interning, a CPython optimization for small integers and strings. This will further deepen our understanding of the is vs. == distinction and reveal another layer of how Python manages objects in memory for efficiency.