Python Dictionary Deep Dive: Hashing, Ordering, and Advanced Patterns
A Python dictionary is a hash map โ it maps keys to values using a hash function to find storage positions. Understanding what this means in practice helps you use dicts more effectively and avoid subtle bugs around hashability and performance.
Creating Dictionaries
# Direct literalperson = {"name": "Alice", "age": 30, "role": "engineer"}
# dict() constructor โ from keyword argumentssettings = dict(host="localhost", port=5432, debug=False)
# dict() from an iterable of key-value pairspairs = [("a", 1), ("b", 2), ("c", 3)]from_pairs = dict(pairs)
# Dict comprehensionsquares = {x: x**2 for x in range(1, 6)}print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Filter with comprehensionhigh_scores = {k: v for k, v in {"Alice": 85, "Bob": 92, "Carol": 78}.items() if v >= 90}print(high_scores) # {'Bob': 92}Dict Ordering in Python 3.7+
Since Python 3.7, dicts maintain insertion order. This is guaranteed by the language specification, not just a CPython implementation detail.
d = {}d["third"] = 3d["first"] = 1d["second"] = 2
print(list(d.keys())) # ['third', 'first', 'second'] โ insertion orderprint(list(d.values())) # [3, 1, 2]print(list(d.items())) # [('third', 3), ('first', 1), ('second', 2)]Accessing and Modifying
config = {"timeout": 30, "retries": 3, "debug": False}
# Access โ raises KeyError if missingprint(config["timeout"]) # 30
# Safe access with defaultprint(config.get("host", "localhost")) # "localhost" โ no error
# setdefault โ set and return default if key missing, otherwise return existingconfig.setdefault("port", 5432) # adds port=5432 if not presentprint(config["port"]) # 5432
# Update from another dictconfig.update({"timeout": 60, "log_level": "INFO"})print(config)Merging Dictionaries
Python 3.9 introduced the | merge operator:
defaults = {"color": "blue", "size": "medium", "weight": 1.0}user_prefs = {"color": "red", "size": "large"}
# Python 3.9+ โ creates a new merged dict, right side wins on conflictsmerged = defaults | user_prefsprint(merged)# {'color': 'red', 'size': 'large', 'weight': 1.0}
# In-place updatedefaults |= user_prefs # modifies defaults in place
# Pre-3.9 equivalentmerged_old = {**defaults, **user_prefs}Common Dict Patterns
Grouping items by a key:
from collections import defaultdict
transactions = [ {"user": "Alice", "amount": 50}, {"user": "Bob", "amount": 30}, {"user": "Alice", "amount": 75}, {"user": "Bob", "amount": 20},]
by_user = defaultdict(list)for t in transactions: by_user[t["user"]].append(t["amount"])
print(dict(by_user))# {'Alice': [50, 75], 'Bob': [30, 20]}
# Totals per usertotals = {user: sum(amounts) for user, amounts in by_user.items()}print(totals) # {'Alice': 125, 'Bob': 50}Inverting a dict (swap keys and values):
original = {"a": 1, "b": 2, "c": 3}inverted = {v: k for k, v in original.items()}print(inverted) # {1: 'a', 2: 'b', 3: 'c'}# Note: only works cleanly when values are unique and hashableSafely accessing nested dicts:
# Manual approachdata = {"user": {"profile": {"name": "Alice"}}}name = data.get("user", {}).get("profile", {}).get("name", "Unknown")print(name) # "Alice"
# Using a helperdef safe_get(d, *keys, default=None): """Safely traverse a nested dict.""" for key in keys: if not isinstance(d, dict): return default d = d.get(key, default) return d
print(safe_get(data, "user", "profile", "name")) # "Alice"print(safe_get(data, "user", "address", "city")) # NoneIterating Correctly
inventory = {"apple": 5, "banana": 3, "cherry": 12}
# Keys (default iteration)for key in inventory: print(key)
# Valuesfor value in inventory.values(): print(value)
# Key-value pairs โ the most common patternfor item, count in inventory.items(): print(f"{item}: {count}")
# Do NOT modify a dict while iterating โ raises RuntimeError# Correct approach: iterate over a copyfor key in list(inventory.keys()): if inventory[key] < 5: del inventory[key] # safe โ iterating over a copy of keysWhat Keys Must Be
Dict keys must be hashable. Strings, numbers, tuples of hashable objects โ all fine. Lists and dicts โ not allowed.
# Valid keysd = { "string_key": 1, 42: 2, (1, 2): 3, # tuple of ints โ hashable True: 4, # bool is hashable (note: True == 1, so True and 1 are the same key)}
# Invalidtry: bad = {[1, 2]: "value"} # list is not hashableexcept TypeError as e: print(f"Cannot use list as key: {e}")Note: True == 1 in Python, so {True: "a", 1: "b"} contains only one key with value "b".