Skip to Content
Course content

238: The copy Module Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely already encountered the idea of copying a list or a dictionary, but in a real production environment, the distinction between a shallow copy and a deep copy is where most "impossible to find" bugs live. I've seen senior devs spend hours chasing a bug where a value changed in one part of the app, only to realize they had a shared reference to a nested list they thought they had duplicated.

The trap of the shallow copy

Imagine you're building a game where players have a character sheet. This sheet isn't just a flat list of numbers; it's a dictionary containing other lists—like a list of active status effects (buffs). You want to create a "preview" of the character's state after a certain spell is cast, so you can show the player the projected changes without actually applying them yet.

import copy

player_state = {
    "name": "Valerius",
    "stats": {"hp": 100, "mp": 50},
    "buffs": ["Shield", "Haste"]
}

# The naive approach: shallow copy
preview_state = player_state.copy()
preview_state["buffs"].append("Berserk")

print(f"Preview: {preview_state['buffs']}") 
# Expected: ['Shield', 'Haste', 'Berserk']
print(f"Original: {player_state['buffs']}") 
# Wait... why is Berserk here too?

Here is what happened: .copy() (and copy.copy()) creates a new dictionary object, but it doesn't create new versions of the objects inside that dictionary. It just copies the references. Both player_state and preview_state are now pointing to the exact same list in memory for the "buffs" key. When you appended "Berserk" to the preview, you mutated the original player state. This is a classic shallow copy failure.

Isolating nested data with deepcopy

When you're dealing with nested structures—lists within lists, or dictionaries within lists—you need copy.deepcopy(). This function doesn't just copy the top-level container; it recursively walks through the entire object tree and creates brand new copies of every single object it finds.

import copy

player_state = {
    "name": "Valerius",
    "stats": {"hp": 100, "mp": 50},
    "buffs": ["Shield", "Haste"]
}

# The better way for nested data
preview_state = copy.deepcopy(player_state)
preview_state["buffs"].append("Berserk")

print(f"Preview: {preview_state['buffs']}") # ['Shield', 'Haste', 'Berserk']
print(f"Original: {player_state['buffs']}") # ['Shield', 'Haste'] - Safe!

Now the two states are entirely decoupled. You can mutate the preview as much as you want without worrying about corrupting the actual game state. I usually reach for deepcopy the moment I see a dictionary containing another mutable object, just to be safe.

The performance tax

Now, I have to give you a warning: deepcopy is not free. Because it has to recursively traverse the object and keep track of everything it has already copied (to avoid infinite loops with self-referencing objects), it is significantly slower than a shallow copy. If you're doing this inside a tight loop—say, 60 times a second in a game engine—your frame rate will tank.

The trade-off is simple: use .copy() for flat data or when you specifically want shared references for memory efficiency. Use deepcopy() when data integrity is more important than a few microseconds of CPU time. If you find yourself calling deepcopy on a massive object every few milliseconds, that's usually a sign that your data architecture is too bulky and you should probably be using immutable types (like tuples) or a more specialized state-management pattern.




📋 Practical Task

Fixing the Nested Configuration Corruption

You are working on a system that manages server configurations. The configurations are stored in a nested dictionary. Currently, the system uses a shallow copy to create a "temporary override" for testing a new config, but this is accidentally corrupting the master configuration.

Your Task: Fix the apply_temporary_override function so that modifying the temp_config does not affect the master_config. Use the copy module correctly to ensure the nested "settings" dictionary is fully decoupled.

import copy

master_config = {
    "server_name": "Production_Main",
    "settings": {
        "timeout": 30,
        "max_connections": 1000,
        "debug_mode": False
    },
    "tags": ["stable", "primary"]
}

def apply_temporary_override(config):
    # BUG: This current implementation is too shallow!
    temp_config = config.copy() 
    
    # Simulate a temporary change for testing
    temp_config["settings"]["debug_mode"] = True
    temp_config["settings"]["timeout"] = 60
    
    return temp_config

# Testing the fix
override = apply_temporary_override(master_config)

print(f"Override Debug Mode: {override['settings']['debug_mode']}") # Should be True
print(f"Master Debug Mode: {master_config['settings']['debug_mode']}") # Should be False

if master_config["settings"]["debug_mode"] == False:
    print("Success: Master config remains untouched!")
else:
    print("Failure: Master config was corrupted!")
Rating
0 0

There are no comments for now.

to be the first to leave a comment.