What is the practical difference between shallow copy and deep copy in Python
What is the practical difference between shallow copy and deep copy in Python
Introduction

When working with Python collections (lists, dictionaries, sets, etc.), understanding how copying works is crucial for avoiding bugs. Python has two types of copying: shallow copy and deep copy. This tutorial will explain the difference with detailed examples and practical use cases.
Table of Contents
- The Problem with Direct Assignment
- What is a Shallow Copy?
- What is a Deep Copy?
- Visual Representation
- Creating Copies in Python
- Detailed Examples with Lists
- Examples with Nested Structures
- Performance Considerations
- When to Use Each
- Common Pitfalls and Solutions
The Problem with Direct Assignment
Let's start with the most common mistake: using direct assignment (=)
when you need a copy.
Example 1: Lists with Direct Assignment
To run the code create a new Python file main.py and execute the following command:
python3 main.py
# Direct assignment - creates a reference, NOT a copy
original_list = [1, 2, 3, 4, 5]
assigned_list = original_list # This creates a reference, not a copy!
print("Original list:", original_list)
print("Assigned list:", assigned_list)
print("Same object?", original_list is assigned_list) # True - same object
# Modify the assigned list
assigned_list[0] = 999
print("\nAfter modification:")
print("Original list:", original_list) # Also changed! [999, 2, 3, 4, 5]
print("Assigned list:", assigned_list) # [999, 2, 3, 4, 5]
What happened?
- original_list and assigned_list point to the same object in memory
- Changing one changes the other
- This is because Python variables are references to objects
Visualizing References
Memory Layout:
┌─────────────┐
│ original_list│───┐
└─────────────┘ │
│ ┌─────────────┐
┌─────────────┐ │───►│ List Object │
│ assigned_list│───┘ │ [1,2,3,4,5] │
└─────────────┘ └─────────────┘
Both variables point to the same list object.
What is a Shallow Copy?
A shallow copy creates a new container object, but populates it with references to the same objects found in the original.
Creating Shallow Copies
import copy
# Method 1: Using copy module
original = [1, 2, 3, 4, 5]
shallow_copy1 = copy.copy(original)
# Method 2: Using slicing (for lists)
shallow_copy2 = original[:]
# Method 3: Using list() constructor
shallow_copy3 = list(original)
# Method 4: Using copy() method (lists, dicts)
shallow_copy4 = original.copy()
print("Original:", original)
print("Shallow copy 1:", shallow_copy1)
print("Are they the same object?", original is shallow_copy1) # False
print("Do they have equal values?", original == shallow_copy1) # True
Shallow Copy with Simple Objects
# Shallow copy works well for flat lists with immutable values
original = [1, 2, 3, 4, 5]
shallow = original.copy()
# Modify the shallow copy
shallow[0] = 999
print("Original:", original) # [1, 2, 3, 4, 5] - unchanged!
print("Shallow copy:", shallow) # [999, 2, 3, 4, 5]
print("Success! They are independent for immutable values.")
The Problem with Shallow Copy: Nested Objects
# Shallow copy creates problems when working with nested mutable objects
original = [1, 2, [3, 4], 5]
shallow = original.copy()
print("Before modification:")
print("Original:", original) # [1, 2, [3, 4], 5]
print("Shallow:", shallow) # [1, 2, [3, 4], 5]
# Modify the nested list in the shallow copy
shallow[2][0] = 999
print("\nAfter modifying nested list:")
print("Original:", original) # [1, 2, [999, 4], 5] - OOPS, changed!
print("Shallow:", shallow) # [1, 2, [999, 4], 5]
print("\nThe nested list is still shared!")
Why did this happen?
- The outer list is a new object
- But the inner list [3, 4] is the same object in both lists
What is a Deep Copy?
A deep copy creates a new container object and recursively copies all objects found in the original.
Creating Deep Copies
import copy
original = [1, 2, [3, 4], 5]
deep_copy = copy.deepcopy(original)
print("Before modification:")
print("Original:", original) # [1, 2, [3, 4], 5]
print("Deep copy:", deep_copy) # [1, 2, [3, 4], 5]
# Modify the nested list in the deep copy
deep_copy[2][0] = 999
print("\nAfter modifying nested list:")
print("Original:", original) # [1, 2, [3, 4], 5] - unchanged!
print("Deep copy:", deep_copy) # [1, 2, [999, 4], 5]
print("\nSuccess! They are completely independent.")
Visual Representation
Shallow Copy Memory Layout
Original: [1, 2, [3, 4], 5]
┌───┬───┬─────────┬───┐
│ 1 │ 2 │ ↓ │ 5 │
└───┴───┴─────────┴───┘
│
Shallow: [1, 2, [3, 4], 5] │
┌───┬───┬─────────┬───┐ │
│ 1 │ 2 │ ↓ │ 5 │ │
└───┴───┴─────────┴───┘ │
└───────────┘ (same list object)
Deep Copy Memory Layout
Original: [1, 2, [3, 4], 5]
┌───┬───┬─────────┬───┐
│ 1 │ 2 │ ↓ │ 5 │
└───┴───┴─────────┴───┘
│
▼
[3, 4]
Deep: [1, 2, [3, 4], 5]
┌───┬───┬─────────┬───┐
│ 1 │ 2 │ ↓ │ 5 │
└───┴───┴─────────┴───┘
│
▼
[3, 4] (different list object)
Creating Copies in Python
Different Ways to Create Copies
import copy
my_list = [1, 2, [3, 4]]
my_dict = {'a': 1, 'b': [2, 3]}
my_set = {1, 2, 3}
# Shallow copy methods
shallow_list1 = my_list.copy() # Method 1: .copy() method
shallow_list2 = my_list[:] # Method 2: Slicing
shallow_list3 = list(my_list) # Method 3: Constructor
shallow_list4 = copy.copy(my_list) # Method 4: copy.copy()
shallow_dict1 = my_dict.copy() # Dictionaries have .copy()
shallow_dict2 = dict(my_dict) # Constructor works too
shallow_dict3 = copy.copy(my_dict)
shallow_set1 = my_set.copy() # Sets have .copy()
shallow_set2 = set(my_set) # Constructor
shallow_set3 = copy.copy(my_set)
# Deep copy (only one reliable way)
deep_list = copy.deepcopy(my_list)
deep_dict = copy.deepcopy(my_dict)
deep_set = copy.deepcopy(my_set)
print("All shallow copies of list are equal?",
shallow_list1 == shallow_list2 == shallow_list3 == shallow_list4) # True
print("But are they the same object? No, they're different objects with same values")
More Examples with Lists
List of Integers (Immutable)
import copy
# List with immutable objects (integers)
original = [1, 2, 3, 4, 5]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
# Modify first element
shallow[0] = 100
deep[0] = 200
print("Original:", original) # [1, 2, 3, 4, 5]
print("Shallow:", shallow) # [100, 2, 3, 4, 5]
print("Deep:", deep) # [200, 2, 3, 4, 5]
# For immutable objects, shallow and deep work the same
print("\nWith immutable objects, shallow and deep behave identically")
List of Lists (Nested Mutable)
import copy
# List with nested mutable objects
original = [[1, 2], [3, 4], [5, 6]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
print("Original ID of inner list [0]:", id(original[0]))
print("Shallow ID of inner list [0]:", id(shallow[0]))
print("Deep ID of inner list [0]:", id(deep[0]))
print("\nOriginal[0] is shallow[0]?", original[0] is shallow[0]) # True
print("Original[0] is deep[0]?", original[0] is deep[0]) # False
# Modify nested list
shallow[0][0] = 999
deep[1][0] = 888
print("\nAfter modification:")
print("Original:", original) # [[999, 2], [3, 4], [5, 6]] - changed!
print("Shallow:", shallow) # [[999, 2], [3, 4], [5, 6]]
print("Deep:", deep) # [[1, 2], [888, 4], [5, 6]]
Examples with Nested Structures
Dictionary with Lists as Values
import copy
# Dictionary with mutable values
student = {
'name': 'Alice',
'grades': [85, 90, 78],
'info': {
'age': 20,
'courses': ['Math', 'Physics']
}
}
# Shallow copy
shallow_student = student.copy()
# OR: shallow_student = copy.copy(student)
# Deep copy
deep_student = copy.deepcopy(student)
print("Original grades ID:", id(student['grades']))
print("Shallow grades ID:", id(shallow_student['grades'])) # Same as original
print("Deep grades ID:", id(deep_student['grades'])) # Different
# Modify grades in shallow copy
shallow_student['grades'][0] = 100
print("\nAfter modifying shallow copy grades:")
print("Original grades:", student['grades']) # [100, 90, 78] - changed!
print("Shallow grades:", shallow_student['grades']) # [100, 90, 78]
print("Deep grades:", deep_student['grades']) # [85, 90, 78] - unchanged
# Modify nested dictionary in deep copy
deep_student['info']['courses'].append('Chemistry')
print("\nAfter modifying deep copy courses:")
print("Original courses:", student['info']['courses']) # ['Math', 'Physics']
print("Deep courses:", deep_student['info']['courses']) # ['Math', 'Physics', 'Chemistry']
Custom Objects
import copy
class Person:
def __init__(self, name, friends=None):
self.name = name
self.friends = friends if friends is not None else []
def __repr__(self):
return f"Person(name='{self.name}', friends={self.friends})"
# Create original person with friends
alice = Person("Alice")
bob = Person("Bob")
charlie = Person("Charlie")
alice.friends = [bob, charlie]
# Shallow copy
shallow_alice = copy.copy(alice)
# Deep copy
deep_alice = copy.deepcopy(alice)
print("Original:", alice)
print("Shallow copy:", shallow_alice)
print("Deep copy:", deep_alice)
print("\nOriginal friends list ID:", id(alice.friends))
print("Shallow copy friends list ID:", id(shallow_alice.friends)) # Same!
print("Deep copy friends list ID:", id(deep_alice.friends)) # Different
# Add a friend to shallow copy
shallow_alice.friends.append(Person("David"))
print("\nAfter adding David to shallow copy:")
print("Original:", alice) # Now has David too!
print("Shallow copy:", shallow_alice)
# Modify name in deep copy
deep_alice.name = "Alicia"
deep_alice.friends[0].name = "Robert"
print("\nAfter modifying deep copy:")
print("Original:", alice) # Unchanged
print("Deep copy:", deep_alice) # Changed
Mixed Data Structures
import copy
# Complex nested structure
company = {
'name': 'TechCorp',
'departments': [
{
'name': 'Engineering',
'employees': ['Alice', 'Bob', 'Charlie'],
'manager': {'name': 'David', 'team_size': 3}
},
{
'name': 'Sales',
'employees': ['Eve', 'Frank'],
'manager': {'name': 'Grace', 'team_size': 2}
}
],
'locations': ['NYC', 'SF', 'London']
}
# Shallow copy
shallow_company = copy.copy(company)
# Deep copy
deep_company = copy.deepcopy(company)
# Add employee to shallow copy
shallow_company['departments'][0]['employees'].append('Diana')
print("Original first department employees:", company['departments'][0]['employees'])
print("Shallow copy first department employees:", shallow_company['departments'][0]['employees'])
print("Are they the same?", company['departments'][0]['employees'] is
shallow_company['departments'][0]['employees']) # True
# Add location to deep copy
deep_company['locations'].append('Tokyo')
print("\nOriginal locations:", company['locations']) # ['NYC', 'SF', 'London']
print("Deep copy locations:", deep_company['locations']) # ['NYC', 'SF', 'London', 'Tokyo']
Performance Considerations
import copy
import time
# Create a large nested structure
def create_large_structure(depth, width):
if depth == 0:
return list(range(width))
return [create_large_structure(depth-1, width) for _ in range(width)]
# Test performance
large_data = create_large_structure(4, 5) # 5^4 = 625 elements
print("Testing performance with nested structure of 625 elements...")
# Time shallow copy
start = time.time()
shallow_copy = copy.copy(large_data)
shallow_time = time.time() - start
# Time deep copy
start = time.time()
deep_copy = copy.deepcopy(large_data)
deep_time = time.time() - start
print(f"Shallow copy time: {shallow_time:.6f} seconds")
print(f"Deep copy time: {deep_time:.6f} seconds")
print(f"Deep copy is {deep_time/shallow_time:.1f} times slower")
# Memory usage
import sys
print(f"\nOriginal size: {sys.getsizeof(large_data)} bytes")
print(f"Shallow copy size: {sys.getsizeof(shallow_copy)} bytes")
print(f"Deep copy size: {sys.getsizeof(deep_copy)} bytes")
Result:

When to Use Each
Use Shallow Copy When:
Objects contain only immutable data (integers, strings, tuples)
# Perfect for shallow copy
config = {'mode': 'production', 'timeout': 30}
backup = config.copy() # Shallow is fine
You want to share nested objects intentionally
# Template with default values
template = {'settings': default_settings}
user1_config = template.copy()
user2_config = template.copy()
# All share the same default_settings object
Performance is critical and structure is large
You're creating a view with some modifications
# Create a modified view of original data
data = [1, 2, 3, 4, 5]
view = data.copy()
view[0] = 100 # Only affects view
Use Deep Copy When:
You need complete independence from original
# Game state that needs to be saved/restored
game_state = {'players': [player1, player2], 'board': board_state}
saved_state = copy.deepcopy(game_state)
Working with mutable nested objects
# Configuration with nested dictionaries/lists
app_config = {
'database': {'host': 'localhost', 'port': 5432},
'features': ['auth', 'logging', 'cache']
}
test_config = copy.deepcopy(app_config)
test_config['database']['host'] = 'test-host'
Creating snapshots for undo/redo functionality
class Document:
def __init__(self):
self.content = []
self.history = []
def save_state(self):
self.history.append(copy.deepcopy(self.content))
Testing modifications without affecting original
def test_function(data):
test_data = copy.deepcopy(data)
# Modify test_data safely
result = process(test_data)
return result
Common Pitfalls and Solutions
Modifying Shared Nested Objects
# Problem
students = [
{'name': 'Alice', 'grades': []},
{'name': 'Bob', 'grades': []}
]
# WRONG: Shallow copy
group1 = students.copy()
group1[0]['grades'].append(95) # Affects original!
print("Original:", students[0]['grades']) # [95] - OOPS!
# SOLUTION: Deep copy
students = [
{'name': 'Alice', 'grades': []},
{'name': 'Bob', 'grades': []}
]
group1 = copy.deepcopy(students)
group1[0]['grades'].append(95)
print("Original:", students[0]['grades']) # [] - Correct!
Copying Objects with Circular References
import copy
# Circular reference
class Node:
def __init__(self, value):
self.value = value
self.next = None
# Create circular reference
node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1 # Circular!
try:
# This will work - deepcopy handles circular references
copied = copy.deepcopy(node1)
print("Deep copy successful even with circular reference!")
except RecursionError:
print("This would fail without special handling")
# Manual copying would fail:
def manual_copy(node):
new_node = Node(node.value)
new_node.next = manual_copy(node.next) # Recursion never ends!
return new_node
Assuming .copy() Method Does Deep Copy
# Different types have different .copy() behavior
my_dict = {'a': [1, 2, 3]}
my_list = [[1, 2], [3, 4]]
dict_copy = my_dict.copy() # Shallow for dict
list_copy = my_list.copy() # Shallow for list
dict_copy['a'][0] = 999 # Affects original
list_copy[0][0] = 999 # Affects original
print("Always check if .copy() is shallow or deep for each type!")
Summary Table
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| Creates | New container, same inner objects | New container, new inner objects |
| Performance | Fast | Slow (especially for large structures) |
| Memory Usage | Low (shares objects) | High (duplicates everything) |
| Use Case | Flat structures, immutables | Nested mutable structures |
| Python Methods | .copy(), [:], list(), dict(), copy.copy() |
copy.deepcopy() |
| Independence | Outer container only | Complete independence |
| Circular Refs | Preserved | Handled correctly |
Final Thoughts
- Always start with shallow copy - it's faster and often sufficient
- Switch to deep copy only when needed - when you have nested mutable objects
- Test your assumptions - modify the copy and check if original changes
- Document your choice - comment why you chose shallow or deep copy
- Consider alternatives - sometimes immutable data structures or views are better
English
Română