The Secret Life of Python: The Copy Dilemma - Shallow vs Deep Copying Explained
Timothy was tracking down a bizarre bug. "Margaret, I'm going crazy," he said, pointing at his screen. "I made a copy of my data structure, modified the copy, and somehow the original changed too! I thought copying was supposed to prevent that."
Margaret leaned in to look. "Let me guess - you used .copy() on a list containing nested lists or dictionaries?"
"Yes! How did you know?"
"You've discovered the difference between shallow and deep copying," Margaret said. "Welcome to one of Python's most surprising behaviors - where what looks like a copy isn't always a complete copy."
The Problem: Copying Isn't Always Copying
Timothy showed Margaret his confusing code:
def demonstrate_copy_problem():
"""The copy that isn't really a copy"""
# Create nested data structure
original = {
'name': 'Alice',
'scores': [85, 90, 92],
'metadata': {'grade': 'A', 'semester': 'Fall'}
}
# Make a "copy"
modified = original.copy()
# Modify the copy's name
modified['name'] = 'Bob'
print(f"Original name: {original['name']}") # 'Alice' - unchanged ✓
print(f"Modified name: {modified['name']}") # 'Bob' - changed ✓
# Modify the copy's scores
modified['scores'].append(95)
print(f"\nOriginal scores: {original['scores']}") # [85, 90, 92, 95] - CHANGED! ✗
print(f"Modified scores: {modified['scores']}") # [85, 90, 92, 95]
# Modify the copy's metadata
modified['metadata']['grade'] = 'B'
print(f"\nOriginal grade: {original['metadata']['grade']}") # 'B' - CHANGED! ✗
print(f"Modified grade: {modified['metadata']['grade']}") # 'B'
print("\n😱 Modifying the 'copy' changed the original!")
demonstrate_copy_problem()
Output:
Original name: Alice
Modified name: Bob
Original scores: [85, 90, 92, 95]
Modified scores: [85, 90, 92, 95]
Original grade: B
Modified grade: B
😱 Modifying the 'copy' changed the original!
"See?" Timothy exclaimed. "The name changed correctly, but the nested list and dictionary changed in BOTH places!"
Assignment, Shallow Copy, and Deep Copy
Margaret drew three diagrams on paper:
"""
Three ways to "copy" in Python:
1. ASSIGNMENT (=): Creates a new reference to the SAME object
original = [1, 2, 3]
copy = original # Both names point to same list
2. SHALLOW COPY: Creates a new object, but nested objects are SHARED
original = [[1, 2], [3, 4]]
copy = original.copy() # New outer list, but inner lists are shared
3. DEEP COPY: Creates completely independent copies, including all nested objects
import copy
original = [[1, 2], [3, 4]]
copy = copy.deepcopy(original) # Everything is copied
"""
def demonstrate_three_approaches():
"""Show the difference between assignment, shallow, and deep copy"""
import copy
original = [[1, 2], [3, 4]]
# 1. Assignment - just another name for the same object
assigned = original
print("Assignment:")
print(f" original is assigned: {original is assigned}") # True - same object!
assigned[0].append(999)
print(f" After modifying 'assigned': {original}") # [[1, 2, 999], [3, 4]]
print(" ✗ Original was modified!\n")
# Reset
original = [[1, 2], [3, 4]]
# 2. Shallow copy - new outer object, shared inner objects
shallow = original.copy()
print("Shallow copy:")
print(f" original is shallow: {original is shallow}") # False - different objects
print(f" original[0] is shallow[0]: {original[0] is shallow[0]}") # True - same inner list!
shallow[0].append(999)
print(f" After modifying shallow[0]: {original}") # [[1, 2, 999], [3, 4]]
print(" ✗ Original was modified!\n")
# Reset
original = [[1, 2], [3, 4]]
# 3. Deep copy - everything is copied
deep = copy.deepcopy(original)
print("Deep copy:")
print(f" original is deep: {original is deep}") # False - different objects
print(f" original[0] is deep[0]: {original[0] is deep[0]}") # False - different inner lists!
deep[0].append(999)
print(f" After modifying deep[0]: {original}") # [[1, 2], [3, 4]]
print(" ✓ Original unchanged!")
demonstrate_three_approaches()
Output:
Assignment:
original is assigned: True
After modifying 'assigned': [[1, 2, 999], [3, 4]]
✗ Original was modified!
Shallow copy:
original is shallow: False
original[0] is shallow[0]: True
After modifying shallow[0]: [[1, 2, 999], [3, 4]]
✗ Original was modified!
Deep copy:
original is deep: False
original[0] is deep[0]: False
After modifying deep[0]: [[1, 2], [3, 4]]
✓ Original unchanged!
Visualizing Shallow vs Deep Copy
Margaret sketched memory diagrams:
def visualize_shallow_vs_deep():
"""Understand what shallow and deep copy actually do"""
original = {
'name': 'Alice', # Immutable string
'scores': [85, 90, 92], # Mutable list
'address': { # Mutable dict
'city': 'NYC',
'zip': '10001'
}
}
"""
SHALLOW COPY (.copy()):
Original Dict Shallow Copy Dict
┌──────────────┐ ┌──────────────┐
│ 'name' ───>│──┐ │ 'name' ───>│──┐
│ │ │ │ │ │
│ 'scores' ───>│──┼──>│ 'scores' ───>│──┼──> [85, 90, 92] (SHARED!)
│ │ │ │ │ │
│ 'address'───>│──┼──>│ 'address'───>│──┼──> {'city': 'NYC'} (SHARED!)
└──────────────┘ │ └──────────────┘ │
│ │
└──> "Alice" (SHARED, but immutable so safe)
The outer dict is copied, but nested objects are NOT copied.
Both dicts point to the SAME list and SAME nested dict.
"""
import copy
shallow = original.copy()
# Check what's shared
print("Shallow copy - what's shared:")
print(f" Dicts are different objects: {original is not shallow}")
print(f" But 'scores' list is shared: {original['scores'] is shallow['scores']}")
print(f" And 'address' dict is shared: {original['address'] is shallow['address']}")
print(f" String 'name' is shared: {original['name'] is shallow['name']}")
print(" (String sharing is safe because strings are immutable)\n")
"""
DEEP COPY (copy.deepcopy()):
Original Dict Deep Copy Dict
┌──────────────┐ ┌──────────────┐
│ 'name' ───>│──┐ │ 'name' ───>│──┐
│ │ │ │ │ │
│ 'scores' ───>│──┼──>│ 'scores' ───>│──┼──> [85, 90, 92] (NEW COPY!)
│ │ │ │ │ │
│ 'address'───>│──┼──>│ 'address'───>│──┼──> {'city': 'NYC'} (NEW COPY!)
└──────────────┘ │ └──────────────┘ │
│ │
└──> "Alice" └──> "Alice" (shared, but safe)
Everything is recursively copied.
No mutable objects are shared between original and deep copy.
"""
deep = copy.deepcopy(original)
print("Deep copy - nothing mutable is shared:")
print(f" Dicts are different objects: {original is not deep}")
print(f" 'scores' list is NOT shared: {original['scores'] is not deep['scores']}")
print(f" 'address' dict is NOT shared: {original['address'] is not deep['address']}")
visualize_shallow_vs_deep()
Ways to Create Shallow Copies
Timothy asked, "Are there multiple ways to make shallow copies?"
def shallow_copy_methods():
"""Different ways to create shallow copies"""
original_list = [1, [2, 3], 4]
# Method 1: .copy() method
copy1 = original_list.copy()
# Method 2: list() constructor
copy2 = list(original_list)
# Method 3: Slice notation [:]
copy3 = original_list[:]
# Method 4: copy.copy() function
import copy
copy4 = copy.copy(original_list)
# All are shallow copies
print("All methods create shallow copies:")
print(f" original is copy1: {original_list is copy1}") # False
print(f" original[1] is copy1[1]: {original_list[1] is copy1[1]}") # True (shared!)
# Dictionaries
original_dict = {'a': 1, 'b': [2, 3]}
# Method 1: .copy() method
dict_copy1 = original_dict.copy()
# Method 2: dict() constructor
dict_copy2 = dict(original_dict)
# Method 3: Dictionary comprehension
dict_copy3 = {k: v for k, v in original_dict.items()}
# Method 4: copy.copy() function
dict_copy4 = copy.copy(original_dict)
print("\nDict copies are also shallow:")
print(f" original is dict_copy1: {original_dict is dict_copy1}") # False
print(f" original['b'] is dict_copy1['b']: {original_dict['b'] is dict_copy1['b']}") # True
shallow_copy_methods()
When Shallow Copy Is Sufficient
"When should I use shallow copy vs deep copy?" Timothy asked.
def when_shallow_copy_works():
"""Shallow copy is fine when you have no nested mutable objects"""
# Case 1: Simple flat structure with immutables
person = {
'name': 'Alice',
'age': 30,
'city': 'NYC'
}
person_copy = person.copy()
person_copy['name'] = 'Bob'
print("Shallow copy with immutables:")
print(f" Original: {person}") # Unchanged
print(f" Copy: {person_copy}") # Changed
print(" ✓ Works perfectly!\n")
# Case 2: List of immutables
numbers = [1, 2, 3, 4, 5]
numbers_copy = numbers.copy()
numbers_copy[0] = 999
print("Shallow copy of immutable list:")
print(f" Original: {numbers}") # Unchanged
print(f" Copy: {numbers_copy}") # Changed
print(" ✓ Works perfectly!\n")
# Case 3: Tuple (immutable, so assignment is fine!)
coordinates = (10, 20, 30)
coordinates_copy = coordinates # Assignment is safe for immutables
# coordinates_copy[0] = 999 # Can't modify - tuples are immutable!
print("Tuples don't need copying:")
print(f" Original: {coordinates}")
print(" ✓ Immutable, so sharing is safe!")
when_shallow_copy_works()
When You Need Deep Copy
Margaret showed Timothy the danger zones:
import copy
def when_deep_copy_needed():
"""Cases where shallow copy is dangerous"""
# Case 1: Nested lists
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
shallow = matrix.copy()
shallow[0][0] = 999
print("Nested lists with shallow copy:")
print(f" Original: {matrix}") # [[999, 2, 3], [4, 5, 6], [7, 8, 9]] ✗
print(f" Copy: {shallow}")
print(" ✗ Original was modified!\n")
# Reset and use deep copy
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
deep = copy.deepcopy(matrix)
deep[0][0] = 999
print("Nested lists with deep copy:")
print(f" Original: {matrix}") # [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ✓
print(f" Copy: {deep}")
print(" ✓ Original unchanged!\n")
# Case 2: Objects containing mutable attributes
class Player:
def __init__(self, name, inventory):
self.name = name
self.inventory = inventory
player1 = Player("Alice", ["sword", "shield"])
# Shallow copy
import copy as copy_module
player2 = copy_module.copy(player1)
player2.name = "Bob" # This is fine - string is immutable
player2.inventory.append("potion") # This modifies BOTH!
print("Object with mutable attributes:")
print(f" Player1 inventory: {player1.inventory}") # Has potion! ✗
print(f" Player2 inventory: {player2.inventory}")
print(" ✗ Shallow copy shared the inventory list!\n")
# Deep copy
player3 = Player("Charlie", ["sword", "shield"])
player4 = copy.deepcopy(player3)
player4.inventory.append("potion")
print("Object with deep copy:")
print(f" Player3 inventory: {player3.inventory}") # No potion ✓
print(f" Player4 inventory: {player4.inventory}") # Has potion ✓
print(" ✓ Deep copy created independent inventory!")
when_deep_copy_needed()
The Performance Trade-off
"Deep copy sounds safer. Why not always use it?" Timothy asked.
import copy
import time
def performance_comparison():
"""Compare performance of shallow vs deep copy"""
# Small structure
small_data = {'a': 1, 'b': [2, 3], 'c': {'d': 4}}
# Shallow copy timing
start = time.perf_counter()
for _ in range(100000):
copy.copy(small_data)
shallow_time = time.perf_counter() - start
# Deep copy timing
start = time.perf_counter()
for _ in range(100000):
copy.deepcopy(small_data)
deep_time = time.perf_counter() - start
print("Small structure (100,000 copies):")
print(f" Shallow copy: {shallow_time:.4f} seconds")
print(f" Deep copy: {deep_time:.4f} seconds")
print(f" Deep copy overhead: {deep_time / shallow_time:.2f}x slower\n")
# Large nested structure
large_data = {
'level1': {
'level2': {
'level3': {
'data': [[i for i in range(100)] for _ in range(10)]
}
}
}
}
# Shallow copy timing
start = time.perf_counter()
for _ in range(1000):
copy.copy(large_data)
shallow_time = time.perf_counter() - start
# Deep copy timing
start = time.perf_counter()
for _ in range(1000):
copy.deepcopy(large_data)
deep_time = time.perf_counter() - start
print("Large nested structure (1,000 copies):")
print(f" Shallow copy: {shallow_time:.4f} seconds")
print(f" Deep copy: {deep_time:.4f} seconds")
print(f" Deep copy overhead: {deep_time / shallow_time:.2f}x slower")
print("\n💡 Deep copy is significantly slower for complex structures!")
performance_comparison()
Custom Copy Behavior
Margaret showed Timothy how to control copying:
import copy
class CustomCopyable:
"""Class with custom copy behavior"""
def __init__(self, name, data, cache):
self.name = name
self.data = data
self.cache = cache # Expensive to copy
def __copy__(self):
"""Define shallow copy behavior"""
print(f" Custom shallow copy of {self.name}")
# Create new instance with same data references
return CustomCopyable(
self.name,
self.data, # Share data
self.cache # Share cache
)
def __deepcopy__(self, memo):
"""Define deep copy behavior"""
print(f" Custom deep copy of {self.name}")
# Create new instance with deep copied data, but shared cache
return CustomCopyable(
copy.deepcopy(self.name, memo),
copy.deepcopy(self.data, memo),
self.cache # Deliberately share cache even in deep copy!
)
def demonstrate_custom_copy():
"""Show custom copy methods"""
obj = CustomCopyable(
name="Config",
data=[1, 2, 3],
cache={'expensive': 'data'}
)
print("Shallow copy with custom behavior:")
shallow = copy.copy(obj)
print(f" obj.data is shallow.data: {obj.data is shallow.data}")
print(f" obj.cache is shallow.cache: {obj.cache is shallow.cache}")
print("\nDeep copy with custom behavior:")
deep = copy.deepcopy(obj)
print(f" obj.data is deep.data: {obj.data is deep.data}") # False - deep copied
print(f" obj.cache is deep.cache: {obj.cache is deep.cache}") # True - deliberately shared!
print("\n💡 Custom copy methods let you control what gets copied!")
demonstrate_custom_copy()
Circular References
Timothy asked, "What if objects reference each other in a cycle?"
import copy
def handle_circular_references():
"""Deep copy handles circular references correctly"""
class Node:
def __init__(self, value):
self.value = value
self.next = None
def __repr__(self):
return f"Node({self.value})"
# Create circular linked list
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
node3.next = node1 # Circular!
print("Original circular structure:")
print(f" node1.next.next.next is node1: {node1.next.next.next is node1}")
# Deep copy handles this correctly
node1_copy = copy.deepcopy(node1)
print("\nDeep copied circular structure:")
print(f" Maintains circularity: {node1_copy.next.next.next is node1_copy}")
print(f" But independent: {node1_copy is not node1}")
print(f" And nested nodes independent: {node1_copy.next is not node2}")
print("\n💡 Deep copy correctly handles circular references!")
handle_circular_references()
Real-World Bug Examples
Margaret showed Timothy common bugs from production:
import copy
def bug_example_1_default_arguments():
"""Bug: Mutable default arguments"""
def buggy_function(data, options={}):
# ❌ BUG: Modifying mutable default argument
options['processed'] = True
return data, options
result1 = buggy_function([1, 2, 3])
result2 = buggy_function([4, 5, 6])
print("Buggy function with mutable default:")
print(f" Result 1 options: {result1[1]}")
print(f" Result 2 options: {result2[1]}")
print(" ✗ Both share the same dict!\n")
def fixed_function(data, options=None):
# ✓ FIXED: Create new dict if None
if options is None:
options = {}
options['processed'] = True
return data, options
result3 = fixed_function([1, 2, 3])
result4 = fixed_function([4, 5, 6])
print("Fixed function:")
print(f" Result 3 options: {result3[1]}")
print(f" Result 4 options: {result4[1]}")
print(" ✓ Each gets independent dict!")
def bug_example_2_config_modification():
"""Bug: Modifying shared config"""
DEFAULT_CONFIG = {
'debug': False,
'cache_size': 1000,
'features': ['feature1', 'feature2']
}
def create_user_config_buggy(overrides):
# ❌ BUG: Shallow copy with nested mutable
config = DEFAULT_CONFIG.copy()
config.update(overrides)
config['features'].append('user_feature')
return config
user1_config = create_user_config_buggy({'debug': True})
user2_config = create_user_config_buggy({'cache_size': 2000})
print("\nBuggy config creation:")
print(f" DEFAULT_CONFIG features: {DEFAULT_CONFIG['features']}")
print(" ✗ Default config was modified!\n")
# Reset
DEFAULT_CONFIG['features'] = ['feature1', 'feature2']
def create_user_config_fixed(overrides):
# ✓ FIXED: Deep copy
config = copy.deepcopy(DEFAULT_CONFIG)
config.update(overrides)
config['features'].append('user_feature')
return config
user3_config = create_user_config_fixed({'debug': True})
user4_config = create_user_config_fixed({'cache_size': 2000})
print("Fixed config creation:")
print(f" DEFAULT_CONFIG features: {DEFAULT_CONFIG['features']}")
print(" ✓ Default config unchanged!")
def bug_example_3_game_state():
"""Bug: Game state management"""
class GameState:
def __init__(self):
self.players = [
{'name': 'Player1', 'inventory': ['sword']},
{'name': 'Player2', 'inventory': ['bow']}
]
self.score = 0
def save_checkpoint_buggy(self):
# ❌ BUG: Shallow copy
return copy.copy(self)
def save_checkpoint_fixed(self):
# ✓ FIXED: Deep copy
return copy.deepcopy(self)
game = GameState()
# Save with buggy method
checkpoint_buggy = game.save_checkpoint_buggy()
game.players[0]['inventory'].append('shield')
print("\nBuggy checkpoint:")
print(f" Current inventory: {game.players[0]['inventory']}")
print(f" Checkpoint inventory: {checkpoint_buggy.players[0]['inventory']}")
print(" ✗ Checkpoint was modified!\n")
# Reset
game = GameState()
# Save with fixed method
checkpoint_fixed = game.save_checkpoint_fixed()
game.players[0]['inventory'].append('shield')
print("Fixed checkpoint:")
print(f" Current inventory: {game.players[0]['inventory']}")
print(f" Checkpoint inventory: {checkpoint_fixed.players[0]['inventory']}")
print(" ✓ Checkpoint preserved correctly!")
bug_example_1_default_arguments()
bug_example_2_config_modification()
bug_example_3_game_state()
Testing Copy Behavior
Margaret showed testing patterns:
import copy
import pytest
def test_shallow_copy_independence():
"""Test that shallow copy creates new outer object"""
original = [1, 2, 3]
shallow = original.copy()
# Outer objects are different
assert original is not shallow
# But can still be modified independently for simple values
shallow.append(4)
assert len(original) == 3
assert len(shallow) == 4
def test_shallow_copy_shares_nested():
"""Test that shallow copy shares nested mutable objects"""
original = [[1, 2], [3, 4]]
shallow = original.copy()
# Outer lists are different
assert original is not shallow
# But inner lists are shared
assert original[0] is shallow[0]
assert original[1] is shallow[1]
# Modifying nested list affects both
shallow[0].append(999)
assert 999 in original[0]
def test_deep_copy_full_independence():
"""Test that deep copy creates fully independent structure"""
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
# Outer lists are different
assert original is not deep
# Inner lists are also different
assert original[0] is not deep[0]
assert original[1] is not deep[1]
# Modifications don't affect original
deep[0].append(999)
assert 999 not in original[0]
def test_copy_with_immutables():
"""Test that immutables don't need deep copy"""
original = (1, 2, "hello", (3, 4))
# Assignment is safe for immutables
copy_ref = original
assert original is copy_ref
# Can't modify immutables anyway
# original[0] = 999 # TypeError
def test_custom_copy_methods():
"""Test that custom copy methods are called"""
class Tracked:
copy_called = False
deepcopy_called = False
def __copy__(self):
Tracked.copy_called = True
return Tracked()
def __deepcopy__(self, memo):
Tracked.deepcopy_called = True
return Tracked()
obj = Tracked()
copy.copy(obj)
assert Tracked.copy_called
copy.deepcopy(obj)
assert Tracked.deepcopy_called
# Run with: pytest test_copying.py -v
The Library Metaphor
Margaret brought it back to the library:
"Think of copying like making photocopies of library documents," she said.
"Assignment is like giving someone the call number to a book - both people are looking at the same book, not making copies at all.
"Shallow copying is like photocopying a catalog page that lists other books. You get your own copy of the catalog page, so you can write notes on it without affecting the original. But the books listed on both catalog pages are still the same books on the shelf. If someone checks out a book listed on the catalog, it's checked out for everyone.
"Deep copying is like photocopying the entire collection - not just the catalog, but also photocopying every book, article, and reference listed in it, and every document those references cite, recursively. Now you have a completely independent collection. Someone can check out a book from your photocopied collection, and it doesn't affect the original collection at all.
"Shallow copying is fast but risky with nested structures. Deep copying is safe but expensive. Choose based on whether you have nested mutable objects and whether those objects need to be independent."
Decision Tree
Timothy created a decision guide:
"""
COPY DECISION TREE:
START: Do you need to copy?
│
├─ Is the object immutable (int, str, tuple of immutables)?
│ └─> NO COPY NEEDED (assignment is safe)
│
├─ Is it a flat structure (no nested mutable objects)?
│ └─> SHALLOW COPY (.copy(), list(), dict())
│
├─ Does it have nested mutable objects?
│ │
│ ├─ Do nested objects need to be independent?
│ │ └─> DEEP COPY (copy.deepcopy())
│ │
│ └─ Is performance critical and sharing is acceptable?
│ └─> SHALLOW COPY (but document the sharing!)
│
└─ Are you unsure?
└─> Use DEEP COPY (safer default)
PERFORMANCE CONSIDERATIONS:
- Shallow copy: O(n) where n = number of top-level items
- Deep copy: O(n*m) where n = items and m = average nesting depth
- For large nested structures, deep copy can be 10-100x slower
MEMORY CONSIDERATIONS:
- Shallow copy: Minimal memory (only top level)
- Deep copy: Full memory duplication (everything copied)
"""
Common Patterns
Margaret showed production patterns:
import copy
class ConfigManager:
"""Pattern: Safe config management"""
def __init__(self, default_config):
# Store deep copy of default config
self._default = copy.deepcopy(default_config)
def get_config(self, overrides=None):
"""Return config with overrides, without modifying default"""
# Start with deep copy of default
config = copy.deepcopy(self._default)
# Apply overrides if provided
if overrides:
config.update(overrides)
return config
class HistoryManager:
"""Pattern: Undo/redo with snapshots"""
def __init__(self, initial_state):
self.current_state = initial_state
self.history = [copy.deepcopy(initial_state)]
self.current_index = 0
def save_state(self):
"""Save current state for undo"""
# Deep copy to preserve state
snapshot = copy.deepcopy(self.current_state)
self.history.append(snapshot)
self.current_index += 1
def undo(self):
"""Restore previous state"""
if self.current_index > 0:
self.current_index -= 1
# Deep copy from history
self.current_state = copy.deepcopy(self.history[self.current_index])
def redo(self):
"""Restore next state"""
if self.current_index < len(self.history) - 1:
self.current_index += 1
self.current_state = copy.deepcopy(self.history[self.current_index])
def demonstrate_patterns():
"""Show the patterns in action"""
# Config pattern
default_config = {
'debug': False,
'logging': {'level': 'INFO', 'handlers': ['console']}
}
manager = ConfigManager(default_config)
config1 = manager.get_config({'debug': True})
config2 = manager.get_config({'logging': {'level': 'DEBUG'}})
print("Config manager:")
print(f" Config 1 debug: {config1['debug']}")
print(f" Config 2 debug: {config2['debug']}")
print(" ✓ Each config is independent\n")
# History pattern
game_state = {'player': {'x': 0, 'y': 0}, 'score': 0}
history = HistoryManager(game_state)
game_state['player']['x'] = 10
history.save_state()
game_state['score'] = 100
history.save_state()
print("History manager:")
print(f" Current state: {history.current_state}")
history.undo()
print(f" After undo: {history.current_state}")
history.undo()
print(f" After second undo: {history.current_state}")
print(" ✓ Undo/redo works correctly!")
demonstrate_patterns()
Key Takeaways
Margaret summarized:
"""
COPY DILEMMA KEY TAKEAWAYS:
1. Three levels of copying:
- Assignment (=): No copy, just another reference
- Shallow copy: New outer object, nested objects shared
- Deep copy: Everything copied recursively
2. Shallow copy methods:
- .copy() method
- list() or dict() constructors
- Slice notation [:] for lists
- copy.copy() function
3. Deep copy:
- copy.deepcopy() function
- Recursively copies all nested objects
- Handles circular references correctly
4. When to use shallow copy:
- Flat structures with immutables
- Simple lists or dicts
- Performance is critical and sharing is acceptable
5. When to use deep copy:
- Nested mutable structures
- Need complete independence
- Undo/redo systems
- Config management
- Checkpointing/snapshots
6. Performance trade-offs:
- Shallow: Fast (2-10x faster)
- Deep: Slower but safer for nested structures
- Deep copy can be 10-100x slower for complex structures
7. Common pitfalls:
- Using shallow copy with nested mutables
- Mutable default arguments
- Modifying "copies" that share nested objects
- Assuming .copy() makes everything independent
8. Best practices:
- Default to deep copy when unsure
- Use shallow copy only when you understand what's shared
- Document when objects share references
- Test copy behavior, especially with nested structures
- Use custom __copy__ and __deepcopy__ for complex objects
"""
Timothy nodded, understanding. "So shallow copy is like copying the table of contents but sharing the chapters. Deep copy is like photocopying the entire book, including all chapters. And I need to choose based on whether I need the nested parts to be independent."
"Exactly," Margaret confirmed. "And when in doubt, use deep copy. It's safer, and premature optimization is the root of all evil. Only switch to shallow copy when you've profiled and know the performance matters more than safety."
With that understanding, Timothy could now safely copy data structures without the surprise of shared nested objects causing mysterious bugs.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
.jpeg)

Comments
Post a Comment