|$ curl https://forge-ai.dev/api/markdown?path=docs/python/dicts
$cat docs/python-—-dictionaries.md
updated Recently·18 min read·published

Python — Dictionaries

PythonIntermediate
Introduction

Dictionaries (dict) are Python's built-in mapping type. They store key-value pairs with O(1) average-time lookups, inserts, and deletes. Since Python 3.7, dictionaries preserve insertion order, making them predictable and versatile for a wide range of applications — from configuration stores to memoization caches to structured data records.

Dict Creation

Python offers multiple ways to create dictionaries, each suited to different use cases. The literal syntax is most common, but factory functions and comprehensions provide concise alternatives.

creation.py
Python
1# Literal syntax — most common and readable
2empty = {}
3user = {"name": "Alice", "age": 30, "role": "admin"}
4
5# dict() constructor — keyword args (keys must be valid identifiers)
6user = dict(name="Alice", age=30, role="admin")
7
8# dict() from iterable of pairs
9pairs = [("name", "Alice"), ("age", 30)]
10user = dict(pairs) # {"name": "Alice", "age": 30}
11
12# dict.fromkeys — all keys map to the same value
13keys = ["x", "y", "z"]
14d = dict.fromkeys(keys, 0) # {"x": 0, "y": 0, "z": 0}
15
16# dict.fromkeys — default None
17d = dict.fromkeys(keys) # {"x": None, "y": None, "z": None}
18
19# zip — parallel sequences into key-value pairs
20names = ["alice", "bob", "charlie"]
21ages = [30, 25, 35]
22users = dict(zip(names, ages))
23# {"alice": 30, "bob": 25, "charlie": 35}
24
25# Dict comprehension — flexible and expressive
26squares = {x: x ** 2 for x in range(5)}
27# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
28
29# Comprehension with filter
30even_squares = {x: x ** 2 for x in range(10) if x % 2 == 0}
31# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
32
33# Nested dict comprehension
34matrix = {(i, j): i * j for i in range(3) for j in range(3)}

info

Prefer literal {} syntax for static data. Use dict() with keyword args when keys are valid identifiers. Use comprehensions when transforming existing sequences. Avoid dict.fromkeys with mutable values — all keys share the same object.
Accessing Values

Python provides bracket notation for direct access and two safe methods — get() and setdefault() — for handling missing keys gracefully.

accessing.py
Python
1d = {"name": "Alice", "age": 30}
2
3# Bracket access — raises KeyError if missing
4print(d["name"]) # Alice
5# print(d["missing"]) # KeyError: 'missing'
6
7# .get() — returns None (or default) if missing
8print(d.get("name")) # Alice
9print(d.get("missing")) # None
10print(d.get("missing", "N/A")) # "N/A"
11
12# .setdefault() — returns value if exists, else inserts default
13d.setdefault("role", "user") # inserts "role": "user"
14d.setdefault("name", "Bob") # name exists, no-op (returns "Alice")
15print(d) # {"name": "Alice", "age": 30, "role": "user"}
16
17# .setdefault() with mutable default — single object, careful!
18d.setdefault("tags", []).append("admin")
19d.setdefault("tags", []).append("staff")
20print(d["tags"]) # ["admin", "staff"]
21
22# Check existence before access
23if "name" in d:
24 print(d["name"]) # Alice
25
26# Nested safe access pattern
27config = {"database": {"host": "localhost", "port": 5432}}
28host = config.get("database", {}).get("host", "localhost")

best practice

Use .get() for lookup with fallback. Use .setdefault() when you need to initialize a missing key and immediately use its value. For simple existence checks, key in d is clearer than d.get(key) followed by is None.
Modifying Dictionaries

Dictionaries are mutable. You can add, update, and remove entries with several methods and operators.

modifying.py
Python
1d = {"a": 1, "b": 2}
2
3# Add or update — bracket assignment
4d["c"] = 3 # {"a": 1, "b": 2, "c": 3}
5d["a"] = 10 # {"a": 10, "b": 2, "c": 3}
6
7# .update() — merge another dict or iterable of pairs
8d.update({"d": 4, "e": 5})
9d.update(f=6, g=7) # keyword args
10d.update([("h", 8), ("i", 9)]) # iterable of pairs
11
12# .pop() — remove and return value (raises KeyError if missing)
13val = d.pop("a") # 10
14val = d.pop("zzz", "fallback") # "fallback" (no error)
15
16# .popitem() — remove and return last inserted pair (LIFO)
17key, val = d.popitem() # ("i", 9) in 3.7+
18
19# .clear() — remove all items
20d.clear() # {}
21
22# del statement — delete a key
23d2 = {"x": 1, "y": 2, "z": 3}
24del d2["y"] # {"x": 1, "z": 3}
25
26# Safe deletion with pop
27d2.pop("missing", None) # None, no error
28
29# Conditional deletion
30if "x" in d2:
31 del d2["x"]

info

Use .pop() with a default when you're not sure the key exists. Use .popitem() for consuming a dict in LIFO order (useful in graph traversal and state machines). Prefer del when you don't need the removed value.
Iteration

Dictionaries support three iteration views: keys, values, and items. Since Python 3.7, iteration order matches insertion order.

iteration.py
Python
1d = {"name": "Alice", "age": 30, "role": "admin"}
2
3# Iterate over keys (default)
4for key in d:
5 print(key) # name → age → role
6
7# .keys() — explicit key iteration
8for key in d.keys():
9 print(key)
10
11# .values() — iterate over values
12for val in d.values():
13 print(val) # Alice → 30 → admin
14
15# .items() — iterate over key-value pairs (most common)
16for key, val in d.items():
17 print(f"{key}={val}")
18
19# Unpacking keys
20k1, k2, k3 = d # k1="name", k2="age", k3="role"
21
22# Convert views to lists
23key_list = list(d.keys())
24val_list = list(d.values())
25item_list = list(d.items())
26
27# Reverse iteration
28for key in reversed(d):
29 print(key) # role → age → name
30
31# Sorted iteration
32for key in sorted(d):
33 print(key) # age → name → role
34
35# Filtered iteration
36for key, val in d.items():
37 if isinstance(val, str):
38 print(f"{key}: {val}")
39
40# Destructuring in comprehensions
41result = {k: v.upper() if isinstance(v, str) else v for k, v in d.items()}
Dict Views (Dynamic)

The objects returned by .keys(), .values(), and .items() are dynamic views — they reflect changes to the underlying dict without creating a new object.

views.py
Python
1d = {"a": 1, "b": 2}
2
3keys = d.keys()
4values = d.values()
5items = d.items()
6
7print(list(keys)) # ["a", "b"]
8
9# Modify the dict — views update automatically
10d["c"] = 3
11del d["a"]
12
13print(list(keys)) # ["b", "c"] — dynamic!
14print(list(items)) # [("b", 2), ("c", 3)]
15
16# Set-like operations on .keys() (Python 3+)
17d1 = {"a": 1, "b": 2, "c": 3}
18d2 = {"b": 20, "c": 30, "d": 40}
19
20print(d1.keys() & d2.keys()) # {"b", "c"} — intersection
21print(d1.keys() | d2.keys()) # {"a", "b", "c", "d"} — union
22print(d1.keys() - d2.keys()) # {"a"} — difference
23print(d1.keys() ^ d2.keys()) # {"a", "d"} — symmetric diff
24
25# .items() also supports set operations (values must be hashable)
26print(d1.items() & d2.items()) # empty — values differ
27
28# .values() does NOT support set ops (values may not be hashable)
29
30# Iteration safety — avoid modifying size during iteration
31d = {"a": 1, "b": 2, "c": 3}
32# for k in d: # RuntimeError if you add/delete
33# if k == "b":
34# del d[k] # !!! RuntimeError: dict changed size
35
36# Safe — iterate over a copy
37for k in list(d):
38 if k == "b":
39 del d[k] # OK — iterating over copy

best practice

Use view set operations (&, |, -, ^) for comparing dict keys — they avoid creating intermediate sets. Never mutate a dict while iterating over its views directly; iterate over a copy (list(d)) instead.
Merging Dictionaries

Python 3.9 introduced the | operator for merging dicts. Prior versions relied on update() or the {**d1, **d2} spread pattern.

merging.py
Python
1a = {"x": 1, "y": 2}
2b = {"y": 3, "z": 4}
3
4# | operator (Python 3.9+) — returns new dict
5merged = a | b # {"x": 1, "y": 3, "z": 4}
6merged = b | a # {"x": 1, "y": 2, "z": 4} (right side wins)
7
8# |= — update in-place (Python 3.9+)
9a |= b # a is now {"x": 1, "y": 3, "z": 4}
10
11# ** spread (Python 3.5+) — creates new dict
12merged = {**a, **b}
13merged = {**a, "extra": 42, **b}
14
15# .update() — in-place merge
16a.update(b)
17
18# Merge with comprehension — transform during merge
19keys = {"x": 1, "y": 2}
20overrides = {"y": 99}
21merged = {k: overrides.get(k, v) for k, v in {**keys, **overrides}.items()}
22
23# Deep merge helper (shallow merge only)
24def deep_merge(base: dict, overlay: dict) -> dict:
25 result = base.copy()
26 for key, val in overlay.items():
27 if key in result and isinstance(result[key], dict) and isinstance(val, dict):
28 result[key] = deep_merge(result[key], val)
29 else:
30 result[key] = val
31 return result
32
33config = {"db": {"host": "localhost", "port": 5432}}
34env = {"db": {"port": 15432}}
35merged = deep_merge(config, env)
36# {"db": {"host": "localhost", "port": 15432}}

info

Use | (Python 3.9+) for simple merges — it is the most readable. Use {**a, **b} when you need to add extra keys inline. For nested dicts, write a recursive merge — none of the built-in approaches do deep merging.
collections Module

Python's collections module provides specialized dict variants for common patterns: defaultdict, Counter, OrderedDict, and ChainMap.

defaultdict

A defaultdict calls a factory function to supply missing keys automatically, eliminating the need for explicit checks.

defaultdict.py
Python
1from collections import defaultdict
2
3# List factory — group items
4words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
5groups = defaultdict(list)
6for word in words:
7 groups[len(word)].append(word)
8# {5: ["apple", "apple", "apple"], 6: ["banana", "banana"], 6: ["cherry"]}
9
10# Int factory — count occurrences
11counter = defaultdict(int)
12for word in words:
13 counter[word] += 1
14# {"apple": 3, "banana": 2, "cherry": 1}
15
16# Set factory — collect unique pairs
17pairs = [("a", 1), ("b", 2), ("a", 3)]
18d = defaultdict(set)
19for k, v in pairs:
20 d[k].add(v)
21# {"a": {1, 3}, "b": {2}}
22
23# Custom factory
24from datetime import datetime
25timestamps = defaultdict(datetime.now) # each access sets current time
26
27# Nested defaultdict (auto-vivification)
28nested = defaultdict(lambda: defaultdict(list))
29nested["users"]["alice"].append("login")
30nested["users"]["bob"].append("logout")

Counter

A Counter is a defaultdict(int) subclass specialized for counting hashable objects.

counter.py
Python
1from collections import Counter
2
3# Count elements
4cnt = Counter(["a", "b", "a", "c", "a", "b"])
5# Counter({"a": 3, "b": 2, "c": 1})
6
7# Count from string
8cnt = Counter("abracadabra")
9# Counter({"a": 5, "b": 2, "r": 2, "c": 1, "d": 1})
10
11# Most common
12print(cnt.most_common(2)) # [("a", 5), ("b", 2)]
13
14# Arithmetic
15c1 = Counter(a=3, b=1)
16c2 = Counter(a=1, b=2, c=3)
17print(c1 + c2) # Counter({"a": 4, "b": 3, "c": 3})
18print(c1 - c2) # Counter({"a": 2}) (zeros removed)
19print(c1 & c2) # Counter({"a": 1, "b": 1}) (intersection — min)
20print(c1 | c2) # Counter({"a": 3, "b": 2, "c": 3}) (union — max)
21
22# Total (Python 3.10+)
23print(cnt.total()) # 11
24
25# Elements iterator
26list(Counter(a=3, b=1).elements()) # ["a", "a", "a", "b"]
27
28# Update and subtract
29cnt.update("abc") # add counts
30cnt.subtract("a") # subtract (can go negative)

OrderedDict

Before Python 3.7 guaranteed dict ordering, OrderedDict was the go-to for ordered mappings. It is still useful when order-sensitive equality matters or when you need to move items to the end.

ordereddict.py
Python
1from collections import OrderedDict
2
3# OrderedDict remembers insertion order
4od = OrderedDict()
5od["a"] = 1
6od["b"] = 2
7od["c"] = 3
8print(list(od)) # ["a", "b", "c"]
9
10# Equality is order-sensitive
11a = OrderedDict([("a", 1), ("b", 2)])
12b = OrderedDict([("b", 2), ("a", 1)])
13print(a == b) # False (order matters!)
14
15# Regular dict equality — order-insensitive
16c = {"a": 1, "b": 2}
17d = {"b": 2, "a": 1}
18print(c == d) # True (3.7+ both ordered, but equality ignores order)
19
20# .move_to_end() — useful for LRU-like patterns
21od = OrderedDict.fromkeys(["a", "b", "c", "d"])
22od.move_to_end("a") # "a" moved to end
23print(list(od)) # ["b", "c", "d", "a"]
24od.move_to_end("d", last=False) # "d" moved to front
25print(list(od)) # ["d", "b", "c", "a"]
26
27# .popitem(last=True) — LIFO (default, same as dict 3.7+)
28# .popitem(last=False) — FIFO (OrderedDict-only)
29od.popitem(last=False) # removes ("d", None) — FIFO behavior

ChainMap

A ChainMap groups multiple dicts into a single, updateable view. Lookups search each mapping in order, making it ideal for scoped configuration (e.g., user overrides layered on defaults).

chainmap.py
Python
1from collections import ChainMap
2
3defaults = {"theme": "dark", "lang": "en", "debug": False}
4user = {"theme": "light", "lang": "fr"}
5
6# ChainMap — defaults serve as fallback
7config = ChainMap(user, defaults)
8print(config["theme"]) # "light" (from user)
9print(config["debug"]) # False (from defaults)
10print(config["lang"]) # "fr" (from user)
11
12# Mutations affect only the first mapping
13config["debug"] = True
14print(user) # {"theme": "light", "lang": "fr", "debug": True}
15print(defaults) # unchanged
16
17# .new_child() — add another layer
18cli_args = {"theme": "solarized"}
19config = config.new_child(cli_args)
20print(config["theme"]) # "solarized"
21
22# .parents — skip the first mapping
23parent = config.parents
24print(parent["theme"]) # "light" (from user)
25
26# .maps — access all underlying dicts
27print(config.maps) # [{"theme": "solarized"}, {"theme": "light", ...}, ...]
28
29# Practical: environment config with fallbacks
30import os
31env = ChainMap(os.environ, {"HOME": "/home/default", "PATH": "/usr/bin"})

best practice

Use defaultdict for grouping and counting. Use Counter when you need most_common() or arithmetic between counts. Use OrderedDict only when equality must consider order or you need FIFO popitem(). Use ChainMap for layered configuration scopes.
Dict Comprehensions — Deep Dive

Dict comprehensions are one of Python's most elegant features. They combine iteration, transformation, and filtering into a single expression. Understanding their full power is essential for writing idiomatic Python.

comprehensions_deep.py
Python
1# Basic pattern: {key_expr: val_expr for item in iterable}
2squares = {x: x ** 2 for x in range(5)}
3# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
4
5# With filter condition
6evens = {x: x ** 2 for x in range(10) if x % 2 == 0}
7# {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
8
9# Transform values based on keys
10data = {"a": 1, "b": 2, "c": 3}
11transformed = {k: v * 10 if k != "b" else v for k, v in data.items()}
12# {"a": 10, "b": 2, "c": 30}
13
14# Swap keys and values (values must be hashable)
15inverted = {v: k for k, v in data.items()}
16# {1: "a", 2: "b", 3: "c"}
17
18# Filter + transform from list
19words = ["hello", "world", "hi", "python"]
20lengths = {w: len(w) for w in words if len(w) > 3}
21# {"hello": 5, "world": 5, "python": 6}
22
23# Enumerate into dict
24indexed = {i: char for i, char in enumerate("abc")}
25# {0: "a", 1: "b", 2: "c"}
26
27# Zip two lists into dict with comprehension control
28keys = ["a", "b", "c"]
29vals = [1, 2, 3]
30d = {k: v for k, v in zip(keys, vals) if v > 1}
31# {"b": 2, "c": 3}
32
33# Nested comprehension — flatten and transform
34matrix = [[1, 2], [3, 4], [5, 6]]
35indexed = {(i, j): val for i, row in enumerate(matrix) for j, val in enumerate(row)}
36# {(0, 0): 1, (0, 1): 2, (1, 0): 3, (1, 1): 4, (2, 0): 5, (2, 1): 6}
37
38# Comprehension with conditional expression (not filter)
39d = {k: ("even" if v % 2 == 0 else "odd") for k, v in {"a": 1, "b": 2}.items()}
40# {"a": "odd", "b": "even"}
41
42# Group items by computed key
43items = ["apple", "banana", "apricot", "blueberry", "cherry"]
44grouped = {item[0]: [x for x in items if x.startswith(item[0])] for item in items}
45# {"a": ["apple", "apricot"], "b": ["banana", "blueberry"], "c": ["cherry"]}
46
47# Dict from two iterables with defaultdict-like behavior via comprehension
48from itertools import groupby
49sorted_items = sorted(items, key=lambda x: x[0])
50grouped = {k: list(g) for k, g in groupby(sorted_items, key=lambda x: x[0])}
51
52# Conditional key inclusion
53d = {f"key_{i}": i for i in range(10) if i % 2 == 0 if i > 2}
54# {"key_4": 4, "key_6": 6, "key_8": 8} (both conditions must pass)
55
56# Using walrus operator (3.8+) in comprehensions
57d = {x: y for x in range(5) if (y := x ** 2) > 5}
58# {3: 9, 4: 16}

info

Dict comprehensions are slightly faster than manual loops due to C-level iteration. However, avoid overly complex comprehensions — if the logic spans more than two lines, a for loop is more readable. Use intermediate variables for clarity.
Nested Dictionaries

Nested dicts are common for representing structured data like JSON objects, configuration trees, or hierarchical records. They require careful access and mutation patterns.

nested.py
Python
1# Nested dict literal
2config = {
3 "database": {
4 "host": "localhost",
5 "port": 5432,
6 "credentials": {
7 "user": "admin",
8 "password": "secret",
9 },
10 },
11 "logging": {
12 "level": "INFO",
13 "file": "/var/log/app.log",
14 },
15}
16
17# Safe nested access
18port = config.get("database", {}).get("port", 5432)
19
20# Nested assignment
21config["database"]["credentials"]["password"] = "new_secret"
22
23# Check nested key existence
24if "database" in config and "credentials" in config["database"]:
25 print(config["database"]["credentials"]["user"])
26
27# Using try/except for nested access (EAFP style)
28try:
29 user = config["database"]["credentials"]["user"]
30except KeyError:
31 user = "default"
32
33# Flatten nested dict with dot notation
34def flatten(d: dict, parent_key: str = "") -> dict:
35 items = []
36 for k, v in d.items():
37 new_key = f"{parent_key}.{k}" if parent_key else k
38 if isinstance(v, dict):
39 items.extend(flatten(v, new_key).items())
40 else:
41 items.append((new_key, v))
42 return dict(items)
43
44flat = flatten(config)
45# {"database.host": "localhost", "database.port": 5432, ...}
46
47# Unflatten back
48def unflatten(d: dict) -> dict:
49 result = {}
50 for key, val in d.items():
51 parts = key.split(".")
52 current = result
53 for part in parts[:-1]:
54 current = current.setdefault(part, {})
55 current[parts[-1]] = val
56 return result
57
58# Nested defaultdict for auto-vivification
59from collections import defaultdict
60nested = defaultdict(lambda: defaultdict(dict))
61nested["users"]["alice"]["role"] = "admin"
62nested["users"]["bob"]["role"] = "viewer"

warning

Nested dict access chains like d["a"]["b"]["c"] raise KeyError if any intermediate key is missing. Use get() with {} fallbacks, try/except, or a flatten/unflatten utility for safe access.
Performance Considerations

Dictionaries are implemented as hash tables, giving O(1) average time complexity for most operations. However, performance degrades with poor hash distribution, high load factors, or very large dicts.

OperationAverageWorst Case
Get / Set / DelO(1)O(n)
Membership (in)O(1)O(n)
Iteration (keys/values/items)O(n)O(n)
Copy (.copy())O(n)O(n)
Merge (| or update)O(len(other))O(len(other))
performance.py
Python
1# Dict vs list membership — dramatic difference
2import timeit
3
4n = 100_000
5data_list = list(range(n))
6data_dict = {i: i for i in range(n)}
7
8# List membership: O(n)
9# timeit.timeit(lambda: 99_999 in data_list, number=100)
10# → ~0.3s
11
12# Dict membership: O(1)
13# timeit.timeit(lambda: 99_999 in data_dict, number=100)
14# → ~0.00001s (30,000x faster)
15
16# Memory — dicts use more memory than lists
17# ~50-100 bytes per entry vs ~8 bytes per list item
18# Trade-off: speed for memory
19
20# Hash collisions degrade performance
21# Custom objects with poor __hash__ implementations
22class BadKey:
23 def __hash__(self):
24 return 42 # all keys collide!
25 def __eq__(self, other):
26 return isinstance(other, BadKey)
27
28d = {BadKey(): i for i in range(1000)}
29# Lookup degrades to O(n) — all keys in same bucket
30
31# Good practice: use immutable, built-in types as keys
32# str, int, tuple, frozenset — all well-distributed

info

Use dict membership (key in d) instead of list membership for large collections — it is orders of magnitude faster. Be mindful of memory: dicts are memory-optimized in CPython 3.6+ (compact table), but still heavier than lists.
Advanced Topics

Key Requirements — Hashable Types

Dict keys must be hashable — they need a stable __hash__() and a working __eq__(). Immutable built-in types (str, int, float, tuple of hashables, frozenset) are safe. Mutable types (list, dict, set) are not.

hashable_keys.py
Python
1# Valid keys — hashable types
2d = {}
3d["string"] = 1 # str
4d[42] = 2 # int
5d[3.14] = 3 # float
6d[(1, 2)] = 4 # tuple (if elements are hashable)
7d[frozenset({1})] = 5 # frozenset
8d[True] = 6 # bool (bool is subclass of int)
9d[None] = 7 # NoneType
10
11# Invalid — unhashable types
12# d[[1, 2]] = 1 # TypeError: unhashable type: 'list'
13# d[{"a": 1}] = 2 # TypeError: unhashable type: 'dict'
14# d[{1, 2}] = 3 # TypeError: unhashable type: 'set'
15
16# Tuple with unhashable element → unhashable
17# d[([1], 2)] = 1 # TypeError
18
19# Custom class — hashable by default (uses id)
20class Point:
21 def __init__(self, x, y):
22 self.x, self.y = x, y
23 def __hash__(self):
24 return hash((self.x, self.y))
25 def __eq__(self, other):
26 return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
27
28p = Point(3, 4)
29d[p] = "origin"
30print(d[Point(3, 4)]) # "origin"
31
32# Warning: if __hash__ depends on mutable fields, breakage follows
33# Once inserted, do not mutate the key's hash-relevant attributes

warning

Never use a mutable object as a dict key. If you must use a custom object, ensure its __hash__() is based on immutable fields only. Mutating a key after insertion corrupts the hash table and leads to silent data loss.

The __missing__ Hook

Subclassing dict and defining __missing__ lets you customize what happens when a key is not found. defaultdict uses this internally.

missing_hook.py
Python
1class CaseInsensitiveDict(dict):
2 """Dict with case-insensitive string keys."""
3 def __missing__(self, key):
4 if isinstance(key, str):
5 for k, v in self.items():
6 if isinstance(k, str) and k.lower() == key.lower():
7 return v
8 raise KeyError(key)
9
10d = CaseInsensitiveDict({"Name": "Alice", "Role": "Admin"})
11print(d["name"]) # Alice (via __missing__)
12print(d["NAME"]) # Alice
13print(d["Role"]) # Admin
14print(d["role"]) # Admin
15
16# The __missing__ hook is called only by d[key] and d.get(key)
17# NOT by key in d, d.get(..., default), or iteration
18
19class DefaultDict(dict):
20 """Simplified version of collections.defaultdict."""
21 def __init__(self, default_factory, *args, **kwargs):
22 super().__init__(*args, **kwargs)
23 self.default_factory = default_factory
24
25 def __missing__(self, key):
26 if self.default_factory is not None:
27 self[key] = self.default_factory()
28 return self[key]
29 raise KeyError(key)
30
31d = DefaultDict(list)
32d["users"].append("alice") # __missing__ creates list, then append
33print(d) # {"users": ["alice"]}

Dict as Switch/Case

Before Python 3.10 introduced match/case, dicts were (and still are) used to implement dispatch tables — a clean, extensible alternative to long if/elif chains.

dispatch.py
Python
1# Dict as dispatch table — no if/elif chains
2def handle_create(args): return f"Creating {args}"
3def handle_read(args): return f"Reading {args}"
4def handle_update(args): return f"Updating {args}"
5def handle_delete(args): return f"Deleting {args}"
6
7handlers = {
8 "create": handle_create,
9 "read": handle_read,
10 "update": handle_update,
11 "delete": handle_delete,
12}
13
14def dispatch(command: str, args):
15 handler = handlers.get(command)
16 if handler is None:
17 return f"Unknown command: {command}"
18 return handler(args)
19
20print(dispatch("create", "user")) # "Creating user"
21print(dispatch("delete", "file")) # "Deleting file"
22
23# Dynamic extension — add handlers at runtime
24handlers["archive"] = lambda args: f"Archiving {args}"
25
26# Enum-based dispatch
27from enum import Enum
28
29class Op(Enum):
30 ADD = "+"
31 SUB = "-"
32 MUL = "*"
33 DIV = "/"
34
35operations = {
36 Op.ADD: lambda a, b: a + b,
37 Op.SUB: lambda a, b: a - b,
38 Op.MUL: lambda a, b: a * b,
39 Op.DIV: lambda a, b: a / b if b != 0 else float("inf"),
40}
41
42def calculate(op: Op, a: float, b: float) -> float:
43 func = operations.get(op)
44 if func is None:
45 raise ValueError(f"Unknown operation: {op}")
46 return func(a, b)
47
48# Class-based dispatch with method lookup
49class Dispatcher:
50 def handle(self, action: str, *args):
51 method = getattr(self, f"do_{action}", None)
52 if method is None:
53 raise ValueError(f"No handler for {action}")
54 return method(*args)
55
56 def do_add(self, a, b): return a + b
57 def do_sub(self, a, b): return a - b
58
59d = Dispatcher()
60print(d.handle("add", 3, 4)) # 7

Dict Ordering (3.7+)

Since Python 3.7, dicts preserve insertion order as a language guarantee (CPython 3.6 had this as an implementation detail). This makes dicts predictable across all Python implementations.

ordering.py
Python
1# Insertion order is preserved (Python 3.7+)
2d = {}
3d["z"] = 1
4d["a"] = 2
5d["m"] = 3
6d["b"] = 4
7print(list(d.keys())) # ["z", "a", "m", "b"]
8
9# Update does not change key position (unless key is new)
10d["a"] = 99
11print(list(d.keys())) # ["z", "a", "m", "b"] (unchanged)
12
13# pop then re-insert moves to the end
14val = d.pop("a")
15d["a"] = 100
16print(list(d.keys())) # ["z", "m", "b", "a"] (reinserted at end)
17
18# delete in iteration — must use list(d) or list(d.keys())
19for k in list(d):
20 if k == "z":
21 del d[k]
22
23# Reversed iteration (Python 3.8+)
24print(list(reversed(d))) # ["a", "b", "m"]
25
26# Ordering guarantee affects JSON serialization
27import json
28print(json.dumps({"z": 1, "a": 2, "m": 3}))
29# {"z": 1, "a": 2, "m": 3} (order preserved)
30
31# Order equality vs value equality
32print({"a": 1, "b": 2} == {"b": 2, "a": 1}) # True (value equality)
33# Regular dict == ignores order, unlike OrderedDict
$Blueprint — Engineering Documentation·Section ID: PYTHON-DICTS·Revision: 1.0