Dicts
Python dict basics: safe access with .get(), defaultdict bucketing, merging with |, and the setdefault idiom.
A Python dict is a mapping from keys to values. Keys must be hashable - strings, numbers, and tuples all work, but lists and other dicts do not. You write a dict literal with curly braces: {"a": 1, "b": 2}. Insertion order is preserved (guaranteed since Python 3.7), so iterating a dict always yields keys in the order they were added.
d[key] reads a value and raises KeyError if the key is missing. d.get(key) returns None instead of raising, and d.get(key, default) returns a custom fallback. Use .get() when a missing key is a normal case; use d[key] when missing keys are a bug you want to catch immediately.
d = {"name": "Ada", "lang": "Python"}
d["name"] # "Ada"
d["missing"] # KeyError: 'missing'
d.get("name") # "Ada"
d.get("missing") # None
d.get("missing", 0) # 0
# Membership check
"name" in d # True
"age" in d # False
# Iterating
for key, value in d.items():
print(key, "->", value)collections.defaultdict creates a missing value automatically when you access a key for the first time. Pass it a callable (like list or int) and it calls that callable to build the default. This is the standard way to bucket items into groups without checking "does this key exist yet?" on every iteration.
from collections import defaultdict
# Group words by their first letter
words = ["apple", "avocado", "banana", "blueberry", "cherry"]
groups = defaultdict(list)
for word in words:
groups[word[0]].append(word)
dict(groups)
# {"a": ["apple", "avocado"], "b": ["banana", "blueberry"], "c": ["cherry"]}
# Count occurrences with int default (starts at 0)
counter = defaultdict(int)
for ch in "mississippi":
counter[ch] += 1
dict(counter)
# {"m": 1, "i": 4, "s": 4, "p": 2}Python 3.9 introduced | to merge two dicts into a new one, and |= to merge in place. When keys overlap, the right-hand dict wins. This replaces the older {**a, **b} idiom, which still works but is harder to read at a glance.
defaults = {"timeout": 30, "retries": 3, "debug": False}
overrides = {"timeout": 10, "debug": True}
# Merge into a new dict (right side wins on conflict)
config = defaults | overrides
config # {"timeout": 10, "retries": 3, "debug": True}
# In-place merge (mutates defaults)
defaults |= overrides
# Equivalent older syntax
config = {**defaults, **overrides}
# setdefault: set key only if not already present
config.setdefault("timeout", 60) # returns 10, does not overwrite
config.setdefault("log_level", "INFO") # inserts "INFO"In production
Insertion order is part of the language since Python 3.7 - OrderedDict is now reserved for the rare case needing move_to_end() or order-sensitive equality. The | operator merges dicts non-destructively (3.9+) and replaces {**a, **b} for clarity. dict.setdefault(k, []) is a one-line read-through cache idiom; for hot paths defaultdict(list) is faster because it skips the key lookup. See the read-through cache pattern for how this scales to a production caching layer.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free