Python - Copy a List (Shallow vs Deep)

• Shallow copies duplicate the list structure but reference the same nested objects, causing unexpected mutations when modifying nested elements

Key Insights

• Shallow copies duplicate the list structure but reference the same nested objects, causing unexpected mutations when modifying nested elements • Deep copies recursively duplicate all nested objects, creating completely independent data structures at the cost of performance and memory • The choice between shallow and deep copying depends on your data structure—use shallow for flat lists with immutable elements, deep for nested mutable objects

Understanding List Copying Fundamentals

When you assign one list to another variable in Python, you’re not creating a copy—you’re creating a reference to the same object in memory. This causes both variables to point to identical data.

original = [1, 2, 3, 4]
reference = original
reference.append(5)

print(original)  # [1, 2, 3, 4, 5]
print(reference)  # [1, 2, 3, 4, 5]
print(original is reference)  # True

This behavior is rarely what you want. You need actual copying mechanisms to create independent list objects.

Shallow Copy Methods

Python provides multiple ways to create shallow copies. All produce the same result but with different syntax preferences.

import copy

original = [1, 2, 3, 4]

# Method 1: Slicing
copy1 = original[:]

# Method 2: list() constructor
copy2 = list(original)

# Method 3: copy() method
copy3 = original.copy()

# Method 4: copy.copy()
copy4 = copy.copy(original)

copy1.append(5)
print(original)  # [1, 2, 3, 4]
print(copy1)     # [1, 2, 3, 4, 5]

For flat lists containing immutable objects (integers, strings, tuples), shallow copies work perfectly. The copied list is independent, and modifications don’t affect the original.

The Shallow Copy Problem

Shallow copies fail when your list contains mutable objects like nested lists, dictionaries, or custom objects. The outer list structure is duplicated, but nested objects remain shared references.

original = [[1, 2], [3, 4], [5, 6]]
shallow = original.copy()

# Modifying the outer list structure works fine
shallow.append([7, 8])
print(original)  # [[1, 2], [3, 4], [5, 6]]
print(shallow)   # [[1, 2], [3, 4], [5, 6], [7, 8]]

# Modifying nested objects affects both lists
shallow[0].append(99)
print(original)  # [[1, 2, 99], [3, 4], [5, 6]]
print(shallow)   # [[1, 2, 99], [3, 4], [5, 6], [7, 8]]

This happens because shallow copying creates a new list object but copies only the references to nested objects, not the objects themselves.

original = [[1, 2], [3, 4]]
shallow = original.copy()

print(original[0] is shallow[0])  # True - same object!
print(id(original[0]) == id(shallow[0]))  # True

Deep Copy Implementation

Deep copying recursively duplicates all nested objects, creating a completely independent structure. Use copy.deepcopy() from the copy module.

import copy

original = [[1, 2], [3, 4], [5, 6]]
deep = copy.deepcopy(original)

deep[0].append(99)
print(original)  # [[1, 2], [3, 4], [5, 6]]
print(deep)      # [[1, 2, 99], [3, 4], [5, 6]]

print(original[0] is deep[0])  # False - different objects!

Deep copies work with arbitrarily nested structures, including dictionaries, sets, and custom objects.

import copy

original = {
    'users': [
        {'name': 'Alice', 'scores': [95, 87, 92]},
        {'name': 'Bob', 'scores': [78, 82, 88]}
    ],
    'metadata': {'version': 1, 'tags': ['active', 'production']}
}

deep = copy.deepcopy(original)
deep['users'][0]['scores'].append(100)
deep['metadata']['tags'].append('modified')

print(original['users'][0]['scores'])  # [95, 87, 92]
print(deep['users'][0]['scores'])      # [95, 87, 92, 100]
print(original['metadata']['tags'])    # ['active', 'production']
print(deep['metadata']['tags'])        # ['active', 'production', 'modified']

Performance Considerations

Deep copying is significantly slower and more memory-intensive than shallow copying because it must traverse and duplicate the entire object graph.

import copy
import time

# Create a large nested structure
large_list = [[i] * 100 for i in range(1000)]

# Measure shallow copy
start = time.perf_counter()
shallow = large_list.copy()
shallow_time = time.perf_counter() - start

# Measure deep copy
start = time.perf_counter()
deep = copy.deepcopy(large_list)
deep_time = time.perf_counter() - start

print(f"Shallow copy: {shallow_time:.6f} seconds")
print(f"Deep copy: {deep_time:.6f} seconds")
print(f"Deep copy is {deep_time/shallow_time:.1f}x slower")

On typical hardware, deep copies can be 10-100x slower depending on nesting depth and object complexity.

Custom Objects and Copy Control

When copying custom objects, you can control copy behavior by implementing __copy__() and __deepcopy__() methods.

import copy

class DatabaseConnection:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.connection_id = id(self)
    
    def __copy__(self):
        # Create new instance without copying connection
        return DatabaseConnection(self.host, self.port)
    
    def __deepcopy__(self, memo):
        # Same behavior for deep copy
        return DatabaseConnection(self.host, self.port)

class Application:
    def __init__(self, name, db_connection):
        self.name = name
        self.db = db_connection
        self.cache = {}

original_db = DatabaseConnection('localhost', 5432)
original_app = Application('MyApp', original_db)
original_app.cache['key1'] = 'value1'

copied_app = copy.deepcopy(original_app)

print(original_app.db.connection_id)  # Different ID
print(copied_app.db.connection_id)    # Different ID
print(original_app.db is copied_app.db)  # False

The memo dictionary in __deepcopy__() prevents infinite recursion when objects contain circular references.

Handling Circular References

Deep copy handles circular references automatically, while manual copying would cause infinite recursion.

import copy

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

# Create circular reference
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
node3.next = node1  # Circular reference

# Deep copy handles this correctly
copied_node1 = copy.deepcopy(node1)

print(copied_node1.value)              # 1
print(copied_node1.next.value)         # 2
print(copied_node1.next.next.value)    # 3
print(copied_node1.next.next.next.value)  # 1 (circular)
print(copied_node1 is node1)           # False

Practical Decision Framework

Choose shallow copy when:

  • Your list contains only immutable objects (numbers, strings, tuples)
  • You need to modify the list structure without affecting nested objects
  • Performance is critical and you understand the reference behavior

Choose deep copy when:

  • Your list contains nested mutable objects (lists, dicts, custom objects)
  • You need complete independence between original and copy
  • You’re working with complex data structures from external sources
import copy

# Shallow copy is fine here
config_options = ['debug', 'verbose', 'production']
user_options = config_options.copy()

# Deep copy required here
user_profile = {
    'name': 'Alice',
    'preferences': {'theme': 'dark', 'notifications': True},
    'history': [{'action': 'login', 'timestamp': '2024-01-01'}]
}
backup_profile = copy.deepcopy(user_profile)

Understanding the distinction between shallow and deep copying prevents subtle bugs in production code, especially when working with complex data structures or APIs that return nested objects.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.