Python - Add/Update Items in Dictionary
The simplest way to add or update dictionary items is through direct key assignment. This approach works identically whether the key exists or not.
Key Insights
- Python dictionaries support multiple methods for adding and updating items, including direct assignment,
update(), merge operators (|,|=), andsetdefault()for conditional insertion - The
update()method and merge operators handle bulk operations efficiently, while direct assignment works best for single key-value pairs - Understanding the differences between shallow and deep updates becomes critical when working with nested dictionaries
Direct Assignment for Single Items
The simplest way to add or update dictionary items is through direct key assignment. This approach works identically whether the key exists or not.
user = {"name": "Alice", "age": 30}
# Add new item
user["email"] = "alice@example.com"
# Update existing item
user["age"] = 31
print(user)
# Output: {'name': 'Alice', 'age': 31, 'email': 'alice@example.com'}
Direct assignment provides O(1) average time complexity and clear, readable syntax. Use this for single operations where you want to explicitly set a value regardless of whether the key exists.
Using update() for Multiple Items
The update() method merges another dictionary or iterable of key-value pairs into the existing dictionary. This method modifies the dictionary in-place and returns None.
config = {"host": "localhost", "port": 8080}
# Update with another dictionary
config.update({"port": 9000, "debug": True})
print(config)
# Output: {'host': 'localhost', 'port': 9000, 'debug': True}
# Update with keyword arguments
config.update(timeout=30, retries=3)
print(config)
# Output: {'host': 'localhost', 'port': 9000, 'debug': True, 'timeout': 30, 'retries': 3}
# Update with list of tuples
config.update([("ssl", True), ("cert_path", "/etc/ssl")])
print(config)
# Output: {'host': 'localhost', 'port': 9000, 'debug': True, 'timeout': 30,
# 'retries': 3, 'ssl': True, 'cert_path': '/etc/ssl'}
The update() method accepts dictionaries, iterables of key-value pairs, or keyword arguments. It’s particularly useful when merging configuration objects or applying batch updates from external sources.
Dictionary Merge Operators (Python 3.9+)
Python 3.9 introduced the merge (|) and update (|=) operators for dictionaries, providing a more intuitive syntax for combining dictionaries.
defaults = {"theme": "dark", "font_size": 12, "autosave": True}
user_prefs = {"theme": "light", "font_size": 14}
# Merge operator creates new dictionary
merged = defaults | user_prefs
print(merged)
# Output: {'theme': 'light', 'font_size': 14, 'autosave': True}
print(defaults) # Original unchanged
# Output: {'theme': 'dark', 'font_size': 12, 'autosave': True}
# Update operator modifies in-place
defaults |= user_prefs
print(defaults)
# Output: {'theme': 'light', 'font_size': 14, 'autosave': True}
The merge operator (|) creates a new dictionary, leaving operands unchanged. The update operator (|=) modifies the left operand in-place. Both operators follow right-to-left precedence—values from the right dictionary override those on the left.
Conditional Updates with setdefault()
The setdefault() method adds a key-value pair only if the key doesn’t exist. If the key exists, it returns the existing value without modification.
cache = {"user_123": {"name": "Bob", "score": 100}}
# Key doesn't exist - adds default value
result = cache.setdefault("user_456", {"name": "Unknown", "score": 0})
print(result)
# Output: {'name': 'Unknown', 'score': 0}
print(cache)
# Output: {'user_123': {'name': 'Bob', 'score': 100},
# 'user_456': {'name': 'Unknown', 'score': 0}}
# Key exists - returns existing value, no update
result = cache.setdefault("user_123", {"name": "Alice", "score": 50})
print(result)
# Output: {'name': 'Bob', 'score': 100} # Original value unchanged
This method is particularly useful for initializing nested structures or implementing caching patterns where you want to avoid overwriting existing data.
Working with Nested Dictionaries
Updating nested dictionaries requires careful consideration of whether you want shallow or deep updates.
# Shallow update - replaces entire nested dictionary
original = {
"database": {
"host": "localhost",
"port": 5432,
"credentials": {"user": "admin", "password": "secret"}
}
}
updates = {
"database": {
"port": 5433
}
}
original.update(updates)
print(original)
# Output: {'database': {'port': 5433}}
# Note: 'host' and 'credentials' are lost!
For deep updates that preserve nested structures, implement a recursive merge function:
def deep_update(base_dict, update_dict):
"""Recursively update nested dictionaries."""
for key, value in update_dict.items():
if isinstance(value, dict) and key in base_dict and isinstance(base_dict[key], dict):
deep_update(base_dict[key], value)
else:
base_dict[key] = value
return base_dict
original = {
"database": {
"host": "localhost",
"port": 5432,
"credentials": {"user": "admin", "password": "secret"}
}
}
updates = {
"database": {
"port": 5433,
"credentials": {"user": "root"}
}
}
deep_update(original, updates)
print(original)
# Output: {'database': {'host': 'localhost', 'port': 5433,
# 'credentials': {'user': 'root', 'password': 'secret'}}}
Bulk Operations with Dictionary Comprehensions
Dictionary comprehensions provide a functional approach to creating updated dictionaries based on transformations or conditions.
prices = {"apple": 1.20, "banana": 0.50, "orange": 0.80}
# Apply 10% discount
discounted = {item: price * 0.9 for item, price in prices.items()}
print(discounted)
# Output: {'apple': 1.08, 'banana': 0.45, 'orange': 0.72}
# Conditional updates
inventory = {"apple": 50, "banana": 0, "orange": 30, "grape": 0}
in_stock = {item: qty for item, qty in inventory.items() if qty > 0}
print(in_stock)
# Output: {'apple': 50, 'orange': 30}
# Transform keys and values
user_input = {"NAME": "alice", "EMAIL": "ALICE@EXAMPLE.COM"}
normalized = {k.lower(): v.lower() for k, v in user_input.items()}
print(normalized)
# Output: {'name': 'alice', 'email': 'alice@example.com'}
Performance Considerations
Different update methods have varying performance characteristics depending on the use case.
import timeit
# Setup code
setup = """
base = {str(i): i for i in range(1000)}
updates = {str(i): i * 2 for i in range(500, 1500)}
"""
# Test direct assignment in loop
direct = """
for k, v in updates.items():
base[k] = v
"""
# Test update() method
update_method = """
base.update(updates)
"""
# Test merge operator
merge_op = """
base |= updates
"""
print(f"Direct assignment: {timeit.timeit(direct, setup, number=10000):.4f}s")
print(f"update() method: {timeit.timeit(update_method, setup, number=10000):.4f}s")
print(f"Merge operator: {timeit.timeit(merge_op, setup, number=10000):.4f}s")
For bulk operations, update() and the merge operator typically outperform loops with direct assignment. The merge operator offers the best readability with comparable performance to update().
Practical Pattern: Configuration Management
Combining these techniques enables robust configuration management:
class Config:
def __init__(self, defaults):
self._config = defaults.copy()
def update(self, **kwargs):
"""Update configuration with validation."""
for key, value in kwargs.items():
if key in self._config:
self._config[key] = value
else:
raise KeyError(f"Unknown configuration key: {key}")
def get(self, key, default=None):
"""Get configuration value with fallback."""
return self._config.get(key, default)
def merge(self, other_config):
"""Merge another configuration, keeping existing values."""
for key, value in other_config.items():
self._config.setdefault(key, value)
# Usage
app_config = Config({"debug": False, "port": 8000})
app_config.update(debug=True)
app_config.merge({"port": 9000, "host": "0.0.0.0"}) # port unchanged, host added
print(app_config._config)
# Output: {'debug': True, 'port': 8000, 'host': '0.0.0.0'}
This pattern combines validation, conditional updates, and merging to create a maintainable configuration system that prevents accidental overwrites while allowing controlled updates.