A dict is a mapping from keys to values. Where a list answers "what is at position 3?", a dict answers "what is the value for this key?". Look-ups, insertions and deletions are all O(1) on average, which is why dictionaries are the workhorse container for anything that needs to be addressed by name: configuration, caches, counts, indexes of records, and JSON documents.
You create a dict with curly braces and colons: {"ana": 30, "ben": 25}. You can also call dict(iterable_of_pairs) or dict(a=1, b=2) for keyword construction. Since Python 3.7 dictionaries remember their insertion order, so iteration is predictable: the first key in is the first key out. The old OrderedDict is now only needed for its handful of extra methods.
Keys must be hashable, which in practice means immutable and well-behaved under equality. Strings, numbers, and tuples of hashable items are hashable; lists, sets and other dicts are not. Values have no such restriction; anything can be a value. Duplicate keys don't exist in a dict: assigning to an existing key replaces the value.
The dict literal is only one of several ways to build a dictionary. Dict comprehensions — {k: f(k) for k in keys} — make it trivial to transform or filter a sequence into a mapping in one expression. When you outgrow them, collections.defaultdict and collections.Counter fill specialized roles without reinventing the wheel.
Building dicts from data
The most common builder is dict(list_of_pairs): any iterable of two-element tuples becomes a dict. Combined with zip you can turn two parallel lists into a dict: dict(zip(headers, row)). Dict comprehensions add a transformation step: {name.lower(): role for name, role in people}.
Reach for collections.defaultdict(list) when you need to group items by key: groups[key].append(item) works even if key has not been seen yet. Reach for collections.Counter(iterable) when the whole task is counting occurrences.
Merging and copying
Use {**a, **b} or Python 3.9's a | b to merge two dicts; later keys win. a.update(b) does the same in place. As with lists, copy() creates a shallow copy; copy.deepcopy is needed when values contain mutable objects.
When iterating a dict, remember that modifying it during iteration raises RuntimeError. Build a new dict with a comprehension or collect keys first with list(d).
The essential dictionary operations.
| Tool | Purpose |
|---|---|
dictbuilt-in type | Mapping from keys to values, ordered by insertion. |
d.get(k, default)method | Returns d[k] or default instead of raising. |
d.setdefault(k, v)method | Returns existing value or inserts v and returns v. |
d.update(other)method | Merges another mapping into d in place. |
collections.defaultdictfactory | Auto-creates missing values from a factory. |
collections.Counterclass | Dict subclass for counting hashable items. |
{k: v for ... in ...}comprehension | Builds a dict from an iterable. |
d.items()method | View of (key, value) pairs for iteration. |
Creating and Using Dictionaries code example
The script builds the same mapping in four different ways, then merges, iterates and counts.
# Lesson: Creating and Using Dictionaries
from collections import Counter, defaultdict
literal = {"ana": 30, "ben": 25}
from_pairs = dict([("ana", 30), ("ben", 25)])
from_zip = dict(zip(["ana", "ben"], [30, 25]))
from_kwargs = dict(ana=30, ben=25)
print("all equal:", literal == from_pairs == from_zip == from_kwargs)
extra = {"cai": 40}
merged = literal | extra
print("merged: ", merged)
for name, age in merged.items():
print(f" {name}: {age}")
# Grouping
rows = [("oslo", "ana"), ("rome", "ben"), ("oslo", "cai")]
by_city: defaultdict[str, list[str]] = defaultdict(list)
for city, name in rows:
by_city[city].append(name)
print("by_city: ", dict(by_city))
# Counting
votes = "aaabbcd"
print("counts: ", Counter(votes).most_common())
# Dict comprehension + filter
grown = {n: a + 1 for n, a in merged.items() if a < 40}
print("grown: ", grown)
Read through the script and notice:
1) Four builder styles yield identical dicts, with the literal usually clearest.
2) `a | b` merges without mutating; `update` mutates the left-hand dict.
3) `defaultdict(list)` skips the `if key not in d: d[key] = []` boilerplate.
4) `Counter(...).most_common()` is a one-liner for frequency tables.
Practice dict comprehensions and setdefault.
# Example A: invert a dict (swap keys and values)
src = {"a": 1, "b": 2, "c": 3}
inv = {v: k for k, v in src.items()}
print(inv)
# Example B: group into buckets with setdefault
pairs = [("odd", 1), ("even", 2), ("odd", 3)]
buckets: dict[str, list[int]] = {}
for k, v in pairs:
buckets.setdefault(k, []).append(v)
print(buckets)
These checks enforce the basic invariants.
d = {"a": 1}
d["a"] = 2; assert d["a"] == 2 # overwrites
assert d.get("missing", 0) == 0
d.update({"b": 3}); assert set(d) == {"a", "b"}
assert dict(zip("abc", [1, 2, 3])) == {"a": 1, "b": 2, "c": 3}
Running prints:
all equal: True
merged: {'ana': 30, 'ben': 25, 'cai': 40}
ana: 30
ben: 25
cai: 40
by_city: {'oslo': ['ana', 'cai'], 'rome': ['ben']}
counts: [('a', 3), ('b', 2), ('c', 1), ('d', 1)]
grown: {'ana': 31, 'ben': 26}