Master Python Shallow vs Deep Copy: Avoid Hidden Bugs and Boost Your Code
This article explains the difference between shallow and deep copying in Python, shows why simple copies can unintentionally modify original data, provides visual memory diagrams, demonstrates multiple copying methods with code examples, compares performance, and offers practical guidelines for choosing the right copy technique in real projects.
Introduction
Copying data in Python may seem straightforward, but many developers encounter surprising bugs when modifications to a copied variable affect the original. This article uncovers the pitfalls of shallow copying and shows how mastering both shallow and deep copies can become a powerful tool for memory management and interview preparation.
1. A Confusing Example
original_list = [1, 2, [3, 4]]
copied_list = original_list.copy() # shallow copy
# modify first‑level element
copied_list[0] = 99
# modify second‑level element
copied_list[2][0] = 88
print("Original:", original_list)
print("Copied:", copied_list)Output:
Original: [1, 2, [88, 4]]
Copied: [99, 2, [88, 4]]The change to copied_list[2][0] also altered original_list because the inner list was shared – a classic shallow‑copy pitfall.
2. Visualizing Memory Structure
3. Understanding Objects in Python
Immutable Objects
Numbers (int, float)
Strings (str)
Tuples (tuple)
Booleans (bool)
These objects cannot be altered after creation; any "modification" creates a new object.
Mutable Objects
Lists (list)
Dictionaries (dict)
Sets (set)
Custom objects
These objects can be changed in place, keeping the same memory address.
4. Shallow Copy (Shallow Copy): Surface‑Level Duplication
How to Create a Shallow Copy
import copy
# Method 1: copy module
new_list = copy.copy(original_list)
# Method 2: list's copy() method
new_list = original_list.copy()
# Method 3: slicing
new_list = original_list[:]
# Method 4: constructor
new_list = list(original_list)How Shallow Copy Works
Only the first level of the object is duplicated; deeper levels remain references to the original objects.
Verification Example
original = [1, 2, [3, 4]]
shallow_copied = original.copy()
# modify first level
shallow_copied[0] = 99
print("After first‑level change:", original, shallow_copied)
# modify second level
shallow_copied[2][0] = 88
print("After second‑level change:", original, shallow_copied)Result shows the first‑level change is isolated, while the second‑level change affects the original.
5. Deep Copy (Deep Copy): Complete Separation
How to Create a Deep Copy
import copy
original = [1, 2, [3, 4]]
deep_copied = copy.deepcopy(original)How Deep Copy Works
All nested objects are recursively duplicated, producing a fully independent copy.
Verification Example
original = [1, 2, [3, 4]]
deep_copied = copy.deepcopy(original)
# modify any level
deep_copied[0] = 99
deep_copied[2][0] = 88
print("Original:", original)
print("Deep copy:", deep_copied)The original list remains unchanged, confirming complete independence.
6. Shallow vs Deep Copy Comparison Table
Feature
Shallow Copy
Deep Copy
Copy depth
Only first level
Recursively copies all levels
Memory usage
Less
More
Performance
Faster
Slower (especially with deep nesting)
Independence
Partial (deep layers shared)
Fully independent
Suitable scenarios
Simple data structures
Complex nested structures
7. Practical Scenarios
Scenario 1 – When Shallow Copy Is Sufficient
# Simple list without nesting
simple_list = [1, 2, 3, 4, 5]
shallow_copy = simple_list.copy()
# Shallow copy of a one‑level dict
config = {'debug': True, 'level': 'info'}
config_copy = config.copy()Scenario 2 – When Deep Copy Is Required
import copy
nested_data = {
'name': 'test',
'settings': {
'color': 'blue',
'size': [10, 20, 30]
}
}
independent_copy = copy.deepcopy(nested_data)
# Modify the copy without affecting the original
independent_copy['settings']['size'][0] = 100
print(nested_data['settings']['size'][0]) # still 10Scenario 3 – Copying Custom Objects
class Node:
def __init__(self, value, children=None):
self.value = value
self.children = children if children else []
node1 = Node(1, [Node(2), Node(3)])
# Shallow copy – children list is shared
shallow_node = copy.copy(node1)
shallow_node.children[0].value = 999
print(node1.children[0].value) # 999 (affected)
# Deep copy – completely independent
deep_node = copy.deepcopy(node1)
deep_node.children[0].value = 888
print(node1.children[0].value) # still 2 (unaffected)8. Performance Considerations: Avoid Overusing Deep Copy
Deep copying large data structures can be costly.
import copy, time
# Create a large nested list
big_data = [[i for i in range(1000)] for _ in range(1000)]
start = time.time()
shallow_copy = copy.copy(big_data)
print(f"Shallow copy time: {time.time() - start:.4f} seconds")
start = time.time()
deep_copy = copy.deepcopy(big_data)
print(f"Deep copy time: {time.time() - start:.4f} seconds")Typical output shows shallow copy is orders of magnitude faster.
9. Summary & Best Practices
Understand object structure : Analyze the nesting level before copying.
Choose wisely : Use shallow copy for simple structures, deep copy for complex nested data.
Mind performance : Prefer shallow copy for large datasets unless full independence is required.
Clarify requirements : Sometimes shared references are intentional.
Test your assumptions : Write small snippets to verify copy behavior.
Decision Flow
Need complete independence? → use deepcopy Only first‑level independence? → use copy or .copy() Shared reference is acceptable? → assign directly
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
