Junior 8 min · March 05, 2026

Python Dictionaries — Silent Key Overwrites in Merges

dict.update() silently overwrites duplicate keys with no error or warning.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
Production
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • A Python dictionary stores data in key-value pairs, like a phonebook mapping names to numbers
  • Keys must be immutable (strings, numbers, tuples); values can be anything
  • Access via square brackets throws KeyError; .get() returns None or a default safely
  • Lookups are O(1) average thanks to hash tables—instant regardless of dictionary size
  • In production, avoid modifying a dictionary while iterating over it or risk RuntimeError
  • Biggest mistake? Using a mutable type (like a list) as a key—you get TypeError immediately
✦ Definition~90s read
What is Dictionaries in Python?

Python dictionaries are hash-map data structures that store key-value pairs, providing O(1) average-time lookups, insertions, and deletions. They exist because you need to map unique identifiers (keys) to associated data (values) — like a real-world dictionary mapping words to definitions, or a user ID to a profile object.

Imagine a real-life dictionary: you look up a word (the key) and instantly find its definition (the value).

Unlike lists that use integer indices, dictionaries let you use any immutable type (strings, numbers, tuples) as keys, making them the go-to choice for representing structured, labeled data in Python.

In the ecosystem, dictionaries are the workhorse for JSON-like data, configuration objects, and caching. They're built into the language at the C level (CPython) and optimized for memory and speed — Python 3.6+ maintains insertion order as a CPython implementation detail, standardized in 3.7.

Alternatives include collections.OrderedDict (when you need explicit ordering guarantees), defaultdict (for automatic default values on missing keys), or dict subclasses like Counter. Don't use dictionaries when you need ordered sequences (use lists), when keys are always sequential integers (use lists or arrays), or when you need thread-safe concurrent access (use multiprocessing.Manager.dict or Redis).

A critical gotcha — and the focus of this article — is that dictionary merges silently overwrite duplicate keys. When you use {dict1, dict2}, dict1 | dict2 (Python 3.9+), or dict1.update(dict2), the last-seen key wins with zero warning. This silent overwrite behavior has caused countless production bugs, especially when merging deeply nested configurations or API responses where keys collide unexpectedly.

Understanding this behavior — and how to guard against it with explicit checks or custom merge logic — separates junior from senior Python developers.

Plain-English First

Imagine a real-life dictionary: you look up a word (the key) and instantly find its definition (the value). A Python dictionary works exactly the same way — instead of flipping through pages, Python jumps straight to the answer in microseconds. You could store a person's name as the key and their phone number as the value, or a product name as the key and its price as the value. It's a lookup table, a phonebook, a menu — any time you need to pair a label with a piece of data, a dictionary is your tool.

Every app you've ever used is quietly powered by key-value pairs. When you log into a website, your username is looked up in a giant table to find your password hash. When Spotify loads your profile, it fetches your name, playlist count, and subscription tier all at once — not as a pile of unconnected numbers, but as named, organised data. Python dictionaries are the building block that makes this kind of organised, instant-access data possible in your own code.

Before dictionaries existed in Python (or before programmers reached for them), people stored related data in parallel lists — one list of names, one list of phone numbers, hoping the indexes stayed in sync. This is fragile, confusing, and breaks the moment someone inserts a row in the wrong place. A dictionary solves this by gluing the label and the value together permanently, so they can never drift apart.

By the end of this article you'll know how to create a dictionary from scratch, read and update values safely, loop through it without breaking anything, and avoid the three mistakes that trip up almost every beginner. You'll also know exactly when a dictionary is the right choice — and when it isn't.

How Python Dictionaries Handle Key Overwrites During Merges

A Python dictionary is a hash map that stores key-value pairs with O(1) average lookup, insertion, and deletion. The core mechanic is that keys must be unique and hashable — when you assign a value to an existing key, the old value is silently replaced. This behavior is deterministic and fast, but it can mask bugs when merging dictionaries because there is no warning or error on duplicate keys.

In practice, dictionary merges using {d1, d2}, d1.update(d2), or the | operator (Python 3.9+) all follow the same rule: later keys win. The merge is shallow — nested dictionaries are not recursively merged, they are replaced entirely. This means if two dictionaries share a key whose value is itself a dict, the entire nested structure from the second dict overwrites the first, not just the overlapping sub-keys.

Use dictionary merges when combining configuration defaults with overrides, merging API response payloads, or building composite data structures. The silent overwrite is a feature for layered configs (e.g., base config + environment overrides) but a liability when merging data from untrusted sources or when you expect key uniqueness to be enforced. Always validate key collisions explicitly in critical paths.

Silent Overwrite Trap
The | operator and update() never raise an error on duplicate keys — the last value wins without any notification.
Production Insight
Teams merging user-provided JSON configs with default configs using {defaults, user} silently lose default values when the user sends a key they shouldn't have.
Symptom: production configs behave differently than expected — a missing feature or wrong threshold — with no error in logs.
Rule: always whitelist allowed keys or use a deep merge library (e.g., deepmerge) for nested configs.
Key Takeaway
Dictionary merges are shallow and silently overwrite duplicate keys — later keys always win.
Never merge untrusted data into critical configs without explicit key validation.
For nested structures, use a recursive merge or explicitly handle sub-dicts to avoid data loss.
Python Dict Merge: Key Overwrite Flow THECODEFORGE.IO Python Dict Merge: Key Overwrite Flow How duplicate keys are silently overwritten during merge Start with Two Dicts Source and target dictionaries to merge Merge with | or update() Later keys overwrite earlier ones silently Duplicate Key Found Second dict's value replaces first's Result Dict Only last value for each duplicate key ⚠ Silent overwrite: no error on duplicate keys Use collections.ChainMap or explicit check to avoid data loss THECODEFORGE.IO
thecodeforge.io
Python Dict Merge: Key Overwrite Flow
Dictionaries Python

Creating Your First Dictionary — and Understanding What You're Actually Building

A dictionary in Python is written with curly braces {}. Inside, you place pairs of items separated by a colon — the thing on the left of the colon is the key, the thing on the right is the value. Each pair is separated from the next by a comma.

Think of it this way: the key is the label on a filing cabinet drawer, and the value is the document inside. You always open the drawer by its label — never by guessing which drawer number it is.

Keys must be unique. You can't have two drawers with the same label — Python would just keep the last one you defined and silently drop the earlier one. Keys must also be immutable (unchangeable), which in practice means they're almost always strings or numbers. Values, on the other hand, can be absolutely anything: a number, a string, a list, even another dictionary.

You can also create an empty dictionary with just {} and fill it in later — useful when you're building data dynamically, like reading lines from a file.

create_dictionary.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# A dictionary representing a single student's record
# Keys are strings (labels), values are mixed types
student = {
    "name": "Maria Chen",       # string value
    "age": 21,                  # integer value
    "gpa": 3.85,                # float value
    "is_enrolled": True,        # boolean value
    "courses": ["Math", "CS"]   # list as a value — totally valid!
}

# Print the whole dictionary
print(student)

# Check what type it is
print(type(student))

# Check how many key-value pairs are inside
print("Number of fields:", len(student))
Output
{'name': 'Maria Chen', 'age': 21, 'gpa': 3.85, 'is_enrolled': True, 'courses': ['Math', 'CS']}
<class 'dict'>
Number of fields: 5
Why Mixed Types Are Fine:
Unlike a spreadsheet column (which expects one type), a dictionary's values can be anything. One key can hold a number while the next holds a list. This flexibility is what makes dictionaries perfect for representing real-world objects like users, products, or API responses.
Production Insight
Silent key overwrite is the #1 production bug with dictionaries.
When loading configuration from multiple files, always check for collisions.
Use dict.update() with a merge function that raises on duplicate keys.
Key Takeaway
Dictionaries store key-value pairs with unique, immutable keys.
Use {} for empty dicts—never use dict() for creation unless you need keyword args.
Always validate key uniqueness when merging data from different sources.

Reading, Adding, and Updating Values — The Three Core Operations You'll Use Every Day

Once your dictionary exists, you need to pull data out of it. The basic way is square bracket notation: student["name"] — think of it as typing the drawer label to pop it open.

But there's a trap with square brackets: if the key doesn't exist, Python throws a KeyError and your program crashes. The safer approach is the .get() method, which returns None by default (or a fallback value you choose) instead of crashing. In production code, .get() is almost always the right choice.

Adding a new key-value pair looks identical to updating an existing one — dictionary["new_key"] = value. If the key already exists, the value gets replaced. If it doesn't exist yet, a new pair is created. Python handles both cases with the same syntax, which keeps things simple.

Deleting a pair uses del dictionary["key"] or the .pop() method. The advantage of .pop() is that it returns the value you removed, so you can use it before it's gone — handy when you're moving data between structures.

read_update_dictionary.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
student = {
    "name": "Maria Chen",
    "age": 21,
    "gpa": 3.85
}

# --- READING ---
# Square bracket access — fast, but crashes if key is missing
print(student["name"])   # works fine

# .get() is the safe version — returns None if key doesn't exist
print(student.get("gpa"))           # prints 3.85
print(student.get("major"))         # prints None — no crash!
print(student.get("major", "Undeclared"))  # custom fallback value

# --- ADDING a new key ---
student["major"] = "Computer Science"  # key didn't exist — now it does
print(student)

# --- UPDATING an existing key ---
student["gpa"] = 3.92  # key already exists — value gets replaced
print("Updated GPA:", student["gpa"])

# --- DELETING a key ---
# Option 1: del — removes silently
del student["age"]
print("After del:", student)

# Option 2: .pop() — removes AND returns the value
gpa_at_graduation = student.pop("gpa")
print("Captured GPA before removing:", gpa_at_graduation)
print("Final record:", student)
Output
Maria Chen
3.85
None
Undeclared
{'name': 'Maria Chen', 'age': 21, 'gpa': 3.85, 'major': 'Computer Science'}
Updated GPA: 3.92
After del: {'name': 'Maria Chen', 'gpa': 3.92, 'major': 'Computer Science'}
Captured GPA before removing: 3.92
Final record: {'name': 'Maria Chen', 'major': 'Computer Science'}
Watch Out: KeyError Crashes
Using dict["key"] on a key that doesn't exist raises KeyError and stops your program cold. In any code that handles user input, API responses, or config files — where keys might be absent — always use .get() instead. It's one habit that will save you hours of debugging.
Production Insight
Square bracket access is a common cause of production outages.
When processing API responses, keys can be absent unexpectedly.
Rule: always use .get() for data from external sources; use [] only for internal structures you control.
Key Takeaway
.get() is safer than square brackets for production code.
dict[key] = value adds if missing, updates if exists.
pop(key) returns the removed value—useful for transferring data.

Looping Through a Dictionary — Keys, Values, and Both at Once

Looping over a dictionary is something you'll do constantly — printing a report, transforming data, searching for a specific value. Python gives you three clean methods for this, and knowing which one to use when is a mark of a confident Python developer.

Looping with just for key in dictionary gives you the keys only — the drawer labels. From each key you can look up the value inside the loop if you need it.

The .values() method gives you just the values, no keys — useful when you want to sum up prices or check if a specific value exists anywhere.

The real power move is .items(), which gives you both the key and the value as a pair on every iteration. This is what you'll use 80% of the time because you usually need both. Python unpacks the pair into two variables automatically — for key, value in dictionary.items() — and it reads almost like plain English.

Dictionaries in Python 3.7 and later also maintain insertion order, so the loop will always visit pairs in the order you added them.

loop_through_dictionary.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
product_prices = {
    "Laptop": 999.99,
    "Mouse": 29.99,
    "Keyboard": 79.99,
    "Monitor": 349.99
}

# --- Loop 1: Keys only ---
print("=== Product Names ===")
for product_name in product_prices:
    print("-", product_name)  # each iteration gives us one key

# --- Loop 2: Values only ---
print("\n=== All Prices ===")
total = 0
for price in product_prices.values():
    total += price           # accumulate a running total
print(f"Total inventory value: ${total:.2f}")

# --- Loop 3: Keys AND Values together (the most common pattern) ---
print("\n=== Full Price List ===")
for product_name, price in product_prices.items():  # unpacks each pair
    if price > 100:
        label = "Premium"   # flag expensive items
    else:
        label = "Budget"
    print(f"{product_name}: ${price:.2f} ({label})")

# --- Checking if a key exists before accessing it ---
search_term = "Webcam"
if search_term in product_prices:       # 'in' checks keys by default
    print(f"\nFound {search_term}: ${product_prices[search_term]}")
else:
    print(f"\n'{search_term}' is not in our catalogue.")
Output
=== Product Names ===
- Laptop
- Mouse
- Keyboard
- Monitor
=== All Prices ===
Total inventory value: $1459.96
=== Full Price List ===
Laptop: $999.99 (Premium)
Mouse: $29.99 (Budget)
Keyboard: $79.99 (Budget)
Monitor: $349.99 (Premium)
'Webcam' is not in our catalogue.
Pro Tip: Use 'in' to Check Keys, Not Values
The expression 'key' in my_dict checks keys only — it's O(1) speed (instant, no matter how big the dictionary). To check values, you'd need value in my_dict.values(), which scans every item — much slower on large data. Design your dictionaries so you search by key whenever possible.
Production Insight
Iterating over a dictionary while modifying it causes RuntimeError.
Always iterate over a frozen copy of keys: list(dict.keys()).
For large dictionaries, consider whether you really need .values() or .items() scan.
Key Takeaway
.keys(), .values(), .items() give view objects for iteration.
Use .items() when you need both key and value.
Iterate over a copy if you plan to add or delete items inside the loop.

Nested Dictionaries — Storing Complex, Real-World Data Structures

Real data is rarely flat. A user doesn't just have a name — they have an address, which itself has a street, city, and postcode. A dictionary can hold another dictionary as a value, letting you model this hierarchy naturally.

This is called a nested dictionary. You access values inside it by chaining square brackets or .get() calls: user["address"]["city"]. Each set of brackets digs one level deeper — like opening a folder inside a folder.

Nested dictionaries are everywhere in professional Python: JSON data from an API is almost always a nested dictionary (Python's json module converts it automatically). Configuration files, database records, and game state are all commonly stored this way.

One thing to watch when working with nested data: if an intermediate key doesn't exist, chaining square brackets will crash on the first missing key before it even tries the inner one. Using .get() at each level prevents this — or you can use a try/except block if the structure is deeply uncertain.

nested_dictionary.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# A dictionary of users — each key is a user ID, each value is another dictionary
user_database = {
    "usr_001": {
        "username": "mariachen",
        "email": "maria@example.com",
        "address": {
            "street": "14 Elm Street",
            "city": "Austin",
            "postcode": "73301"
        },
        "is_premium": True
    },
    "usr_002": {
        "username": "jakethompson",
        "email": "jake@example.com",
        "address": {
            "street": "9 Oak Avenue",
            "city": "Denver",
            "postcode": "80201"
        },
        "is_premium": False
    }
}

# Access a top-level value
print(user_database["usr_001"]["username"])  # chain brackets to go deeper

# Access a deeply nested value
print(user_database["usr_001"]["address"]["city"])

# Safe access with .get() at each level — no crash if a key is missing
user = user_database.get("usr_003", {})          # returns empty dict if not found
city = user.get("address", {}).get("city", "Unknown")
print("City for usr_003:", city)                  # gracefully returns 'Unknown'

# Loop through all users and print a summary
print("\n=== User Summary ===")
for user_id, user_info in user_database.items():
    tier = "Premium" if user_info["is_premium"] else "Free"
    city = user_info["address"]["city"]
    print(f"{user_id} | @{user_info['username']} | {city} | {tier}")
Output
mariachen
Austin
City for usr_003: Unknown
=== User Summary ===
usr_001 | @mariachen | Austin | Premium
usr_002 | @jakethompson | Denver | Free
Real-World Connection:
When your Python code calls a REST API and gets back JSON, Python converts it to a nested dictionary automatically. Knowing how to read and navigate nested dictionaries is a direct, on-the-job skill — not just an exam topic.
Production Insight
Chaining .get() calls is fragile for deeply nested data.
Use collections.defaultdict or a recursive helper to safely access nested keys.
Prefer try/except KeyError when the nesting depth is unpredictable.
Key Takeaway
Nested dicts model real hierarchical data like JSON responses.
Safe access: use nested .get() with empty dict fallback.
Consider using dpath library or glom for production-grade nested access.

Dictionary Comprehensions — Build Dictionaries in One Line

Python's dictionary comprehension is a concise way to generate dictionaries from iterables. The syntax is {key: value for item in iterable}. It's the dict equivalent of list comprehensions — and just as powerful.

You can filter items with an if clause at the end. You can also use two for clauses to flatten nested data into a dictionary. Comprehensions are almost always faster than manual loops because they run at C speed inside the interpreter.

But beware: if the comprehension produces duplicate keys, the later one wins — silently. This makes them risky when the key-generating expression isn't guaranteed unique.

dict_comprehensions.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Basic: square each number from 1 to 5
squares = {x: x**2 for x in range(1, 6)}
print(squares)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Filter: only squares of even numbers
squares_even = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(squares_even)  # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

# Transform a list of tuples into a dict
pairs = [("a", 1), ("b", 2), ("c", 3)]
my_dict = {k: v for k, v in pairs}
print(my_dict)  # {'a': 1, 'b': 2, 'c': 3}

# Swap keys and values (if values are unique)
original = {"x": 10, "y": 20}
swapped = {v: k for k, v in original.items()}
print(swapped)  # {10: 'x', 20: 'y'}

# Using a function: convert Celsius to Fahrenheit
celsius = {"Monday": 20, "Tuesday": 25, "Wednesday": 30}
fahrenheit = {day: (temp * 9/5) + 32 for day, temp in celsius.items()}
print(fahrenheit)  # {'Monday': 68.0, 'Tuesday': 77.0, 'Wednesday': 86.0}
Output
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
{2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
{'a': 1, 'b': 2, 'c': 3}
{10: 'x', 20: 'y'}
{'Monday': 68.0, 'Tuesday': 77.0, 'Wednesday': 86.0}
Comprehensions Are Fused Loops
  • Each comprehension compiles to a single bytecode instruction (MAKE_FUNCTION+FOR_ITER), not a loop overhead.
  • If you can express a transformation as a comprehension, do it—it's faster and more readable.
  • But if the logic needs break/continue or side effects, stick with a regular for-loop.
Production Insight
Dict comprehensions can hide duplicate key bugs.
When generating keys from user input, always validate uniqueness first.
For performance-critical code, time your comprehension vs manual loop—sometimes caching introduces overhead.
Key Takeaway
Dict comprehensions build dicts concisely: {k: v for k,v in iterable}.
Add if for filtering; use multiple for clauses for flattening.
Watch for silent duplicate key overwrites in comprehensions.

Why Key Hashing Dictates Performance — and When Your Dict Betrays You

Dictionaries look like magic. They're not. Under the hood, Python runs every key through a hash function to determine where in memory to store the value. That's why lookups are O(1) average — the hash tells the interpreter exactly where to jump. No scanning. No loops. Just math.

But here's where juniors get burned: mutable types like lists can't be keys. Try my_dict[[1, 2, 3]] = 'value' and Python throws a TypeError. Lists aren't hashable because they can change. Tuples? Only if they contain only hashable elements. A tuple with a nested list? Same crash.

This bites you in production when you use a dict as a cache key for complex arguments. If the key object implements __hash__ and __eq__ inconsistently, you'll get silent data corruption — wrong cache hits, missing values, impossible bugs. Always ensure your custom key objects are immutable and implement hash correctly. Or stick to strings, integers, and tuples of immutables. Cheap insurance.

key_hashing_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# io.thecodeforge
def sigma(x):
    return sum(ord(c) for c in str(x))

# Simulating what dict sees: hash determines bucket
class BadKey:
    def __init__(self, val):
        self.val = val
    def __hash__(self):
        return sigma(self.val)
    def __eq__(self, other):
        return self.val == other.val

cache = {}
key = BadKey("order_42")
cache[key] = "pending"

# Changing the value after hashing — corrupts the bucket
key.val = "order_99"
print(cache.get(key, "MISS — key mutated after insertion"))
Output
MISS — key mutated after insertion
Production Trap:
Never mutate an object used as a dict key after insertion. The hash stays in the old bucket. Python won't rehash it. You've just created a ghost entry.
Key Takeaway
Keys must be immutable and hash-stable. If hash can change after insertion, expect silent data loss.

The Three Ways to Delete Data — and Which One Won't Blow Up Your Pipeline

Removing entries from a dict seems trivial. It's not. Three methods exist, and two of them can silently crash a batch job. Here's the truth:

del dict[key] — fastest, zero ceremony. But throws KeyError if the key's missing. In a loop processing 100k records, one missing key kills the entire run. Always wrap in try/except if data is untrusted.

dict.pop(key, default) — your workhorse. Removes the key and returns the value. If key's absent, returns default instead of exploding. Use this when you need to process and remove in one step, like draining a queue.

dict.popitem() — removes and returns the last inserted key-value pair. Useful for LIFO caches or cleanup. In Python 3.7+, dicts preserve insertion order, so popitem() is deterministic. Before 3.7, it returned arbitrary pairs. Don't rely on order for legacy code.

For batch processing, always pop with a default. The two extra characters save a midnight pager call.

safe_deletion.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# io.thecodeforge
order_queue = {
    "ORD-1001": 4,
    "ORD-1002": 12,
    "ORD-1003": 7,
}

# Bad: del crashes on missing key
try:
    del order_queue["ORD-9999"]
except KeyError:
    pass  # swallowing errors — terrible pattern

# Good: pop with default
processed = order_queue.pop("ORD-1001", None)
if processed is not None:
    print(f"Shipped {processed} units from ORD-1001")

# Cleanup: drain remaining in insertion order
while order_queue:
    order_id, units = order_queue.popitem()
    print(f"Drained {order_id}: {units} units")
Output
Shipped 4 units from ORD-1001
Drained ORD-1003: 7 units
Drained ORD-1002: 12 units
Pro Tip:
Use dict.pop(key, None) for idempotent deletion. It returns None on miss instead of crashing. Your data pipeline will thank you.
Key Takeaway
Prefer pop() with a default over del for production code. One missing key shouldn't tank a million-record batch.

Merging Dictionaries Without Losing Data — The Right Way in 2024

You've got two config dicts. One has defaults, one has overrides. You need to merge them. New devs chain .update() or unpack with **. Both can silently overwrite values you wanted to keep. Here's the cold truth:

dict_a.update(dict_b) — modifies dict_a in place. If dict_b has a key that dict_a also has, dict_b wins. No warning. No trace. Your default value is gone.

{defaults, overrides} — creates a new dict. Same overwrite behavior, but at least you're not mutating the original. Still, no protection.

For safe merging, use the merge operator | (Python 3.9+). merged = defaults | overrides does the same overwrite, but reads clearly. However, if you need to preserve all values — say, merging lists instead of overwriting — you must write custom logic.

Production pattern: For nested configs, use collections.ChainMap. It keeps references to original dicts and checks them in order. No mutation, no lost data. Or use a recursive merge function that handles lists by extension, not replacement. Don't learn this after a config bug nukes staging.

merge_safely.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# io.thecodeforge
from collections import ChainMap

defaults = {"host": "localhost", "port": 5432, "debug": False}
overrides = {"port": 8080, "user": "admin"}

# Bad: update mutates and overwrites
config = defaults.copy()
config.update(overrides)  # port changed, debug lost forever if overrides had it

# Good: ChainMap preserves originals without copying
safe_config = ChainMap(overrides, defaults)  # overrides checked first
print(safe_config["host"])   # from defaults
print(safe_config["port"])   # from overrides (8080)
print(safe_config["debug"])  # from defaults (False)

# For merging nested dicts, write a recursive function
def deep_merge(base, overrides):
    merged = base.copy()
    for key, val in overrides.items():
        if key in merged and isinstance(merged[key], dict) and isinstance(val, dict):
            merged[key] = deep_merge(merged[key], val)
        else:
            merged[key] = val
    return merged

conflicting = {"database": {"host": "old", "port": 5432}}
with_overrides = {"database": {"host": "new", "pool": 10}}
print(deep_merge(conflicting, with_overrides))
Output
localhost
8080
False
{'database': {'host': 'new', 'port': 5432, 'pool': 10}}
Production Trap:
.update() mutates the target dict in place. If you're merging configs from multiple sources, use ChainMap or a custom deep_merge to avoid silent key destruction.
Key Takeaway
For safe, non-mutating merges with priority, use ChainMap. For nested dicts, write a recursive merge function. Never rely on .update() in shared state.
● Production incidentPOST-MORTEMseverity: high

Silent Key Overwrite Took Down Our Inventory API

Symptom
After a deployment, certain SKUs consistently showed zero stock even though the database had stock. No errors in logs, no crashes.
Assumption
We assumed the loading code was correct because it had been running for months. We blamed the database or the frontend for displaying stale data.
Root cause
A new configuration file defined inventory thresholds using the same keys as the existing dictionary. Python doesn't raise an error on duplicate keys—the later assignment silently overwrites the earlier one. Our merge logic used dict.update() without checking for collisions.
Fix
We added a validation step that raises a custom exception if a key already exists before merging: compare the config dictionary against a frozen snapshot of the existing keys. Additionally, we switched to using collections.ChainMap for layered configurations so duplicate keys are never overwritten—they shadow but the original remains accessible.
Key lesson
  • Never assume dictionary assignments are safe when merging multiple sources.
  • Use dict.setdefault() or collections.ChainMap when overlapping keys are possible.
  • Always log a warning when a key is overwritten—even if no error is raised.
Production debug guideSymptom-based quick fixes for the most common dictionary failures4 entries
Symptom · 01
KeyError raised when accessing a key
Fix
Use dict.get(key, default) instead of square brackets. If the key is expected but missing, check the source that builds the dictionary for logic errors or missing data.
Symptom · 02
Dictionary size is smaller than expected after insertions
Fix
Check for duplicate keys—Python silently overwrites. Print the keys to see which ones are missing. Consider using defaultdict(list) if you expect multiple values per key.
Symptom · 03
RuntimeError: dictionary changed size during iteration
Fix
Iterate over a snapshot: for key in list(dict.keys()): or collect keys to modify in a separate list then apply changes after the loop.
Symptom · 04
TypeError: unhashable type when using a list as a key
Fix
Convert the mutable object to an immutable one: tuple(my_list) or use a frozenset for collections. If you need a mutable key, redesign the data structure.
★ Dictionary Debug Cheat SheetFast commands to diagnose dictionary behaviour in production scripts or interactive sessions.
KeyError crashing your program
Immediate action
Wrap the access in a try-except KeyError block
Commands
value = my_dict.get(key, 'fallback')
if key in my_dict: value = my_dict[key]
Fix now
Replace all square bracket accesses with .get() for that section
Dictionary seems to be missing some entries+
Immediate action
Print all keys and compare to expected list
Commands
print(sorted(my_dict.keys()))
missing = set(expected_keys) - set(my_dict.keys())
Fix now
Investigate the source that populates the dictionary for overwrite logic
Iteration error 'dictionary changed size during iteration'+
Immediate action
Stop the loop and decide which keys to remove
Commands
for key in list(my_dict.keys()): ...
to_delete = [k for k in my_dict if condition]; for k in to_delete: del my_dict[k]
Fix now
Never modify the dictionary inside the loop; always work on a copy
Feature / AspectPython DictionaryPython List
How you access dataBy a named key: dict['name']By a position number: list[0]
Order guaranteed?Yes — insertion order kept (Python 3.7+)Yes — always ordered by index
Lookup speedO(1) — instant, regardless of sizeO(n) — slower as the list grows (for searching)
Duplicate keys allowed?No — last assignment wins silentlyYes — duplicate values are fine
Best used whenYou need to label your data (name, price, id)You have a sequence of similar items (scores, names)
Key types allowedImmutable only: strings, numbers, tuplesNot applicable — lists use integer indexes
Memory usageSlightly higher due to hash table overheadLower — compact sequential storage

Key takeaways

1
A dictionary stores data as key-value pairs
use it any time your data has labels (name, price, id) rather than just a sequence of items.
2
Always use .get() instead of square brackets when the key might not exist
it prevents KeyError crashes and lets you define a sensible fallback value.
3
Dictionary lookups are O(1)
Python uses a hash table internally, meaning it jumps directly to the value regardless of how many pairs are stored.
4
Nested dictionaries (a dictionary inside a dictionary) are the natural way to model real-world hierarchical data like users, addresses, or JSON API responses.
5
Dict comprehensions are faster and more readable than manual loops for building dictionaries from iterables.

Common mistakes to avoid

3 patterns
×

Using a mutable type (like a list) as a dictionary key

Symptom
You get TypeError: unhashable type: 'list' immediately. The program crashes before it can process any data.
Fix
Convert the list to an immutable tuple first: use tuple([1,2,3]) as the key instead of [1,2,3].
×

Accessing a key that might not exist with square brackets instead of .get()

Symptom
A KeyError crashes your program, especially painful when processing user input or API responses where keys are not guaranteed.
Fix
Switch to .get('key', fallback_value) — it returns None (or your chosen default) instead of crashing.
×

Modifying a dictionary while iterating over it

Symptom
Python raises RuntimeError: dictionary changed size during iteration if you add or delete keys inside a for key in dictionary loop.
Fix
Iterate over a snapshot of the keys: for key in list(dictionary.keys()) — this creates a static copy so adding or removing from the original dictionary during the loop is safe.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What's the difference between dict['key'] and dict.get('key'), and when ...
Q02SENIOR
Dictionaries are described as O(1) for lookups — can you explain why tha...
Q03JUNIOR
If you have two large lists — one of employee names and one of their sal...
Q01 of 03JUNIOR

What's the difference between dict['key'] and dict.get('key'), and when would you choose one over the other in production code?

ANSWER
dict['key'] raises a KeyError if the key is missing, while dict.get('key') returns None (or a custom default). In production, use [] only when you are certain the key exists—e.g., accessing a field you just set. Use .get() for data from external sources like API responses, user input, or config files where keys may be absent. For performance, both are O(1), but .get() has a tiny overhead due to the default handling.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
Can a Python dictionary have duplicate keys?
02
What types can be used as dictionary keys in Python?
03
Is a Python dictionary ordered? Will my keys always come back in the same order?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 24, 2026
last updated
1,554
articles · all by Naren
🔥

That's Data Structures. Mark it forged?

8 min read · try the examples if you haven't

Previous
Tuples in Python
3 / 12 · Data Structures
Next
Sets in Python