Dictionaries are Python's workhorse data structure. Here are techniques to use them effectively.

Safe Access with get()

user = {"name": "Owen", "age": 25}
 
# Risky - raises KeyError
email = user["email"]  # KeyError!
 
# Safe - returns None
email = user.get("email")  # None
 
# Safe - with default
email = user.get("email", "unknown@example.com")

setdefault()

Get value or set and return default:

# Without setdefault
if "tags" not in user:
    user["tags"] = []
user["tags"].append("python")
 
# With setdefault
user.setdefault("tags", []).append("python")

One line instead of three.

defaultdict

Auto-create missing values:

from collections import defaultdict
 
# Regular dict
counts = {}
for word in words:
    if word not in counts:
        counts[word] = 0
    counts[word] += 1
 
# defaultdict
counts = defaultdict(int)
for word in words:
    counts[word] += 1  # Auto-creates 0
 
# Other defaults
lists = defaultdict(list)
sets = defaultdict(set)
nested = defaultdict(dict)

Dictionary Comprehensions

# Basic
squares = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
 
# With condition
evens = {x: x**2 for x in range(10) if x % 2 == 0}
 
# Transform keys and values
upper = {k.upper(): v for k, v in data.items()}
 
# Swap keys and values
inverted = {v: k for k, v in original.items()}

Merging Dictionaries

a = {"x": 1, "y": 2}
b = {"y": 3, "z": 4}
 
# Python 3.9+
merged = a | b  # {"x": 1, "y": 3, "z": 4}
 
# Update in place
a |= b  # a is now {"x": 1, "y": 3, "z": 4}
 
# Older Python
merged = {**a, **b}

Later values override earlier ones.

Iteration Patterns

d = {"a": 1, "b": 2, "c": 3}
 
# Keys
for key in d:
    print(key)
 
# Values
for value in d.values():
    print(value)
 
# Both
for key, value in d.items():
    print(f"{key}: {value}")

Counter

Special dict for counting:

from collections import Counter
 
words = ["apple", "banana", "apple", "cherry", "apple"]
counts = Counter(words)
# Counter({'apple': 3, 'banana': 1, 'cherry': 1})
 
counts.most_common(2)  # [('apple', 3), ('banana', 1)]
counts["apple"]        # 3
counts["missing"]      # 0 (not KeyError)

OrderedDict

Preserves insertion order (dict does too since 3.7, but OrderedDict has extra methods):

from collections import OrderedDict
 
od = OrderedDict()
od["first"] = 1
od["second"] = 2
od.move_to_end("first")  # Move to end
od.popitem(last=False)   # Pop first item

ChainMap

Search multiple dicts:

from collections import ChainMap
 
defaults = {"color": "red", "size": "medium"}
user_prefs = {"color": "blue"}
 
settings = ChainMap(user_prefs, defaults)
settings["color"]  # "blue" (from user_prefs)
settings["size"]   # "medium" (from defaults)

Common Patterns

Group by key

from collections import defaultdict
 
users = [
    {"name": "Alice", "dept": "eng"},
    {"name": "Bob", "dept": "sales"},
    {"name": "Carol", "dept": "eng"},
]
 
by_dept = defaultdict(list)
for user in users:
    by_dept[user["dept"]].append(user)

Invert mapping

original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}

Filter dict

data = {"a": 1, "b": 2, "c": 3, "d": 4}
filtered = {k: v for k, v in data.items() if v > 2}
# {"c": 3, "d": 4}

Nested access

def get_nested(d, *keys, default=None):
    for key in keys:
        if isinstance(d, dict):
            d = d.get(key)
        else:
            return default
    return d if d is not None else default
 
data = {"user": {"profile": {"name": "Owen"}}}
get_nested(data, "user", "profile", "name")  # "Owen"
get_nested(data, "user", "missing", "key")   # None

My Rules

  1. Use get() — avoid KeyError
  2. Use defaultdict — for grouping/counting
  3. Use Counter — for counting specifically
  4. Use comprehensions — for transformations
  5. Use | — for merging (Python 3.9+)

Master dictionaries and you master Python data handling.

React to this post: