The Secret Life of Python: The Copy Dilemma - Shallow vs Deep Copying Explained

 

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.

Comments

Popular posts from this blog

The New ChatGPT Reason Feature: What It Is and Why You Should Use It

Insight: The Great Minimal OS Showdown—DietPi vs Raspberry Pi OS Lite

Raspberry Pi Connect vs. RealVNC: A Comprehensive Comparison