Python List Copy vs Deep Copy: Avoiding Common Pitfalls
Learn the difference between shallow and deep copy in Python lists, when each is appropriate, and how to avoid bugs caused by unintended shared references in nested structures.
Advertisement
You've been there. You copy a list in Python, change something in the copy, and suddenly your original list is also changed. It's confusing, frustrating, and can break your code in subtle ways. Let me walk you through what's really happening and how to avoid these pitfalls.
The Shallow Copy Trap
When you write something like new_list = old_list, you're not actually creating a new list. You're just creating a new reference to the same list object. Any change you make to new_list will also affect old_list. This is the most common mistake beginners make.
original = [1, 2, 3]
copy = original
copy.append(4)
print(original) # Output: [1, 2, 3, 4]
See what happened? You only changed copy, but original changed too. That's because both variables point to the same list in memory.
The Shallow Copy Solution
For simple lists containing only immutable objects like integers or strings, you can use the copy() method or the slicing syntax [:].
original = [1, 2, 3]
shallow_copy = original.copy()
shallow_copy.append(4)
print(original) # Output: [1, 2, 3]
print(shallow_copy) # Output: [1, 2, 3, 4]
This works perfectly for flat lists. But here's where it gets tricky.
When Shallow Copy Fails
Consider a list that contains other lists or mutable objects:
original = [[1, 2], [3, 4]]
shallow_copy = original.copy()
shallow_copy[0].append(5)
print(original) # Output: [[1, 2, 5], [3, 4]]
Wait, what? You only changed the copy, but the original also changed. That's because copy() only creates a new outer list, but the inner lists are still shared between the original and the copy. This is the shallow copy behavior.
Deep Copy to the Rescue
When you need a completely independent copy, including all nested objects, you need deepcopy from the copy module.
import copy
original = [[1, 2], [3, 4]]
deep_copy = copy.deepcopy(original)
deep_copy[0].append(5)
print(original) # Output: [[1, 2], [3, 4]]
print(deep_copy) # Output: [[1, 2, 5], [3, 4]]
Now the original stays untouched. Deep copy recursively copies everything, creating entirely independent objects at every level.
When to Use Each
Use shallow copy when: - Your list contains only immutable objects (integers, strings, tuples) - You want to share nested objects between copies - Performance matters and you're working with large nested structures
Use deep copy when: - Your list contains mutable objects like other lists, dictionaries, or custom objects - You need complete independence between copies - You're modifying nested structures and don't want side effects
Real-World Example from PythonSkillset
At PythonSkillset, we once had a bug where a configuration list was being modified unexpectedly. The code looked something like this:
default_config = [{"port": 8080, "host": "localhost"}, {"port": 9090, "host": "example.com"}]
def get_config():
return default_config.copy() # This is shallow!
config1 = get_config()
config2 = get_config()
config1[0]["port"] = 3000
print(config2[0]["port"]) # Output: 3000 (unexpected!)
The fix was simple: use deepcopy instead.
import copy
def get_config():
return copy.deepcopy(default_config)
Understanding the Difference
Shallow copy creates a new list, but the elements inside are still references to the same objects. If those objects are mutable (like lists, dictionaries, or custom objects), changes will propagate.
Deep copy creates a completely independent copy at every level. Every nested object is also copied, so changes never affect the original.
Practical Examples
Example 1: Working with game state
import copy
initial_board = [["X", "O"], ["O", "X"]]
board_copy = copy.deepcopy(initial_board)
board_copy[0][0] = "O"
print(initial_board) # Output: [['X', 'O'], ['O', 'X']] - unchanged
Example 2: Configuration dictionaries
import copy
base_config = {
"database": {"host": "localhost", "port": 5432},
"cache": {"enabled": True, "ttl": 300}
}
user_config = copy.deepcopy(base_config)
user_config["database"]["port"] = 5433
print(base_config["database"]["port"]) # Output: 5432 - safe!
Performance Considerations
Deep copy is slower and uses more memory because it recursively copies everything. For large nested structures, this can be significant. Use it only when you need true independence.
For simple cases, shallow copy is faster and perfectly fine. The key is knowing when your data structure contains mutable objects.
Common Pitfalls to Watch For
1. Forgetting about nested lists in function arguments
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # Output: [1]
print(add_item(2)) # Output: [1, 2] - unexpected!
The default argument [] is created once and reused. Use None and create a new list inside the function instead.
2. Copying objects with custom classes
class User:
def __init__(self, name, roles):
self.name = name
self.roles = roles
user1 = User("Alice", ["admin", "editor"])
user2 = copy.deepcopy(user1)
user2.roles.append("viewer")
print(user1.roles) # Output: ['admin', 'editor'] - safe!
Performance Tips
Deep copy is expensive. For large nested structures, it can be slow and memory-intensive. Consider these alternatives:
- Use
copy.copy()for shallow copies when you know the structure is flat - Implement
__copy__and__deepcopy__methods in your custom classes for more control - Use
picklefor serialization if you need to copy complex object graphs
The Bottom Line
Always ask yourself: "Do I need the nested objects to be independent?" If yes, use deepcopy. If no, copy() or slicing is fine. When in doubt, deep copy is safer but slower.
Remember this simple rule: shallow copy for flat lists, deep copy for nested structures. Your future self will thank you when debugging those mysterious bugs.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.