Mutating a dictionary—changing its contents after creation—is a fundamental operation in Python. Unlike immutable sequences like tuples, dictionaries are mutable, meaning their key-value pairs can be added, modified, or removed in place. Understanding these operations is crucial for effective and bug-free programming.

Assigning Key-Value Pairs

The most straightforward way to add a new key-value pair or update the value of an existing key is using the square bracket assignment syntax: dict_name[key] = value.

inventory = {'apples': 10, 'oranges': 5}

# Add a new key-value pair
inventory['bananas'] = 15
print(inventory)  # Output: {'apples': 10, 'oranges': 5, 'bananas': 15}

# Update the value of an existing key
inventory['apples'] = 25
print(inventory)  # Output: {'apples': 25, 'oranges': 5, 'bananas': 15}

This operation works because dictionaries are implemented as hash tables. When you assign a value to a key, Python computes the hash of the key to find a “slot” in the underlying array where the value should be stored. If the key already exists, its corresponding value is overwritten. This is an average O(1) time complexity operation.

A common pitfall is assuming that a key exists before trying to update it. Attempting to access a non-existent key for reading raises a KeyError. However, assignment does not have this issue; it will simply create the new key.

The update() Method

For bulk updates or merging dictionaries, the update() method is more efficient than multiple individual assignments. It accepts either another dictionary object, an iterable of key-value pairs, or keyword arguments.

default_settings = {'theme': 'light', 'notifications': True}
user_overrides = {'theme': 'dark', 'language': 'en'}

# Update using another dictionary
default_settings.update(user_overrides)
print(default_settings)
# Output: {'theme': 'dark', 'notifications': True, 'language': 'en'}

# Update using an iterable of key-value tuples
default_settings.update([('font_size', 12), ('notifications', False)])
print(default_settings)
# Output: {'theme': 'dark', 'notifications': False, 'language': 'en', 'font_size': 12}

# Update using keyword arguments
default_settings.update(theme='auto', timeout=30)
print(default_settings)
# Output: {'theme': 'auto', 'notifications': False, 'language': 'en', 'font_size': 12, 'timeout': 30}

The update() method processes its arguments in order. If the same key appears multiple times (e.g., in a list of tuples and a keyword argument), the last processed value wins. This method modifies the original dictionary in place and returns None, a common source of errors for those expecting it to return a new, merged dictionary.

The pop() Method

The pop() method removes a key from the dictionary and returns its corresponding value. It requires the key as its first argument. Its crucial feature is the optional second argument: a default value to return if the key is not found. This prevents a KeyError and makes code more robust.

config = {'host': 'localhost', 'port': 8080, 'debug': True}

# Remove and return the value for an existing key
port_value = config.pop('port')
print(port_value)  # Output: 8080
print(config)      # Output: {'host': 'localhost', 'debug': True}

# Safely remove a potentially missing key with a default
timeout_value = config.pop('timeout', 30)
print(timeout_value)  # Output: 30 (the default, not an error)
print(config)         # Output: {'host': 'localhost', 'debug': True}

# This would raise a KeyError: 'timeout'
# config.pop('timeout')

Using pop() with a default is a best practice when you cannot guarantee a key’s existence. It is the explicit and safe way to remove an item, signaling your intent to both the interpreter and other programmers reading your code.

The del Statement

The del statement is a more imperative way to remove a key-value pair from a dictionary. It is a statement, not a method, and it does not return the value—it simply deletes the association.

user_data = {'name': 'Alice', 'email': 'alice@example.com', 'age': 30}

# Delete a specific key-value pair
del user_data['age']
print(user_data)  # Output: {'name': 'Alice', 'email': 'alice@example.com'}

# Attempting to delete a non-existent key raises a KeyError
try:
    del user_data['phone']
except KeyError as e:
    print(f"KeyError: {e}")  # Output: KeyError: 'phone'

The primary difference from pop() is that del is used when you do not need the value and are certain the key exists. It is less safe than pop() with a default because it offers no built-in way to handle missing keys. Its behavior is linked to Python’s garbage collection; deleting a key removes the reference to the value object, which may then be freed from memory if no other references exist.

Best Practices and Common Pitfalls

  1. Prefer get() for Safe Lookups, pop() for Safe Removal: Use my_dict.get(key, default) when you want to read a value without modifying the dictionary. Use my_dict.pop(key, default) when you want to remove and use a value safely.
  2. Beware of Mutating During Iteration: Modifying a dictionary’s size (by adding or removing keys) while iterating over it will raise a RuntimeError. To safely remove items during iteration, first create a list of keys to remove.
    my_dict = {'a': 1, 'b': 2, 'c': 3, 'bad_key': 99}
    # Correct way to remove items during iteration
    for key in list(my_dict.keys()):  # Use list() to create a static copy
        if key.startswith('bad_'):
            del my_dict[key]
    print(my_dict)  # Output: {'a': 1, 'b': 2, 'c': 3}
    
  3. update() Overwrites Silently: Remember that update() will overwrite existing keys without warning. Always be aware of the contents of the dictionary you are merging in, especially when using user or external input.
  4. Key Type Consistency: While dictionaries can use various immutable types as keys, consistency is a best practice. Using different types for the same logical purpose (e.g., sometimes using the integer 1 and sometimes the string '1' as a key) is a common source of bugs.