items, keys, values
for k, v in d.items():
items() is what you want most of the time. Plain for k in d iterates keys, which is easy to forget and produces a confusing error when you then treat k as a value.
All three are views, not lists: they reflect later changes to the dict and are cheap to create. Wrap in list() if you need a snapshot — and you do need one if you intend to modify the dict while looping.
The counting pattern
The block everyone writes first:
if w in counts:
counts[w] += 1
else:
counts[w] = 1
collapses to:
counts[w] = counts.get(w, 0) + 1
and, if counting is all you are doing, to Counter(words) from collections, which is faster and says what it means.
The grouping pattern
teams.setdefault(team, []).append(name)
setdefault returns the value at that key, inserting the default first if the key was absent. So this reads "get the list for this team, making an empty one if needed, and append to it" — three lines in one.
defaultdict(list) does the same job when every access should create a default. setdefault is better when only some do.
pop and update
d.pop("a") # remove and return; raises if absent
d.pop("a", None) # ...unless given a default
d.update(other) # merge in place
a | b # a new merged dict (3.9+)
With |, the right-hand side wins on conflicts, which makes defaults | overrides a clean way to layer configuration.
Views, and why they are not lists
keys(), values() and items() return views: live windows onto the dictionary rather than snapshots of it. Add a key and every existing view reflects it immediately, because a view holds no data of its own.
That is efficient and it has one consequence worth knowing. Modifying a dictionary while looping over one of its views raises RuntimeError: dictionary changed size during iteration. The fix is to iterate over a snapshot:
for key in list(d):
if should_remove(key):
del d[key]
list(d) copies the keys first, so the loop is walking its own list and the dictionary is free to change underneath.
Key and item views also behave like sets, which is occasionally very handy: d1.keys() & d2.keys() gives the keys both dictionaries share, without a loop.
Order is guaranteed now
Since Python 3.7, dictionaries keep insertion order as a language guarantee rather than an implementation accident. That is why dict.fromkeys(items) is the standard order-preserving deduplication, and why iterating a dictionary produces a stable, predictable sequence.
It does not make a dictionary a substitute for a sorted structure - the order is insertion order, not sorted order - but it does mean you can rely on what you put in first coming out first.
Choosing between get, setdefault and defaultdict
Three tools that overlap, and the distinction is about what should happen when a key is missing.
get reads and never writes. Use it when a missing key has a sensible fallback and you do not want to add anything to the dictionary.
setdefault reads and writes: it inserts the default when the key is absent and returns whatever is now there. Use it for the grouping idiom, where you want the list to exist so you can append to it.
defaultdict(list) moves that behaviour into the dictionary itself, so every missing key springs into existence on access. Use it when *every* access should create a default. Its downside is exactly that: a typo'd key lookup silently creates an empty entry instead of raising, which can mask a bug.
Merging
merged = {**defaults, **overrides} # 3.5+
merged = defaults | overrides # 3.9+
defaults |= overrides # in place
Later keys win in all three. This is the clean way to layer configuration, and it beats a loop of if key not in d conditions because the precedence is stated by the order of the operands rather than buried in the logic.
Counter, and the four things it gives you
Counting is common enough that the standard library has a dictionary subclass for it, and knowing four of its behaviours removes a lot of hand-written code:
from collections import Counter
text = "the cat and the hat and the bat"
counts = Counter(text.split())
print(counts.most_common(2))
print(counts["the"], counts["dog"])
print(sum(counts.values()))
[('the', 3), ('and', 2)]
3 0
8
First, it counts anything iterable in one call, so the loop disappears. Second, most_common(n) sorts by count and gives the top n, which is the question people actually want answered and is otherwise a sorted with a key. Third, a missing key returns 0 rather than raising — and, unlike defaultdict, it does not insert the key on access, so reading does not quietly grow the dictionary. Fourth, counters support arithmetic: a + b merges counts and a - b subtracts them, which is a neat way to diff two collections.
It is still a dictionary, so everything else on this page applies to it.
What can be a key, and why
Any object can be a key if it is hashable, which in practice means immutable all the way down. Strings, numbers, booleans and tuples of those are fine. Lists, dictionaries and sets are not, and a tuple containing a list is not either, because hashing it has to hash its contents.
The reason is how a dictionary works. It computes a hash of the key, uses that to decide where to store the entry, and looks in the same place later. If a key could change after insertion, its hash would change, and the entry would be sitting somewhere the lookup will never search. Rather than allow that, Python refuses to hash mutable types at all.
Two consequences are worth knowing. 1, 1.0 and True all hash the same and compare equal, so they are the *same key* — {1: "a", True: "b"} has one entry. And a tuple key is the standard way to index by more than one thing: grid[(row, col)] is a perfectly ordinary dictionary lookup, and the brackets around the tuple are optional.
Custom classes are hashable by default, using identity, which means two distinct instances with identical contents are different keys. If you want them to be the same key, define __eq__ and __hash__ together — or use a frozen dataclass, which writes both for you.
A dictionary instead of a chain of elif
Once functions can be values, a dictionary replaces a whole shape of code:
def add(a, b): return a + b
def sub(a, b): return a - b
OPS = {"+": add, "-": sub}
print(OPS["+"](3, 4))
print(OPS.get("*", lambda a, b: None)(3, 4))
7
None
Compared with an if/elif chain, this separates the table of options from the code that uses it. Adding an operation is one dictionary entry rather than another branch, the set of valid options can be listed with OPS.keys(), and the same table can drive a help message or validate input.
The pattern generalises well beyond arithmetic: handlers keyed by message type, formatters keyed by file extension, validators keyed by field name. It is the same idea as the menu earlier in the track, and it is worth reaching for whenever a chain of elif is comparing one value against a list of constants.
Where an if chain is still better: when the conditions are not simple equality — ranges, combinations, anything needing and — a dictionary cannot express the question, and forcing it to is worse than the chain.
Getting the items out in a useful order
A dictionary keeps insertion order, which is rarely the order you want to report in. sorted takes the same key argument here as everywhere else, and d.items() gives it pairs to work with.
Sorting by key is sorted(d.items()), which works because tuples compare element by element and the key is first. Sorting by value needs a key function that reaches for the second element — key=lambda kv: kv[1], or key=itemgetter(1) — and adding reverse=True gives you largest first. For a top-n rather than a full sort, heapq.nlargest(3, d.items(), key=...) does less work, and Counter.most_common(3) does it for you when the values are counts.
The result is a list of tuples, not a dictionary. If you need a dictionary back in that order, wrap it: dict(sorted(d.items())) builds a new one, and since 3.7 the insertion order it gets is the sorted order you just produced. This is the standard way to produce a "sorted dictionary" in Python, and it is worth knowing that it is a snapshot — later insertions go on the end, not into their sorted position.
One detail that bites when sorting by value: ties come out in whatever order they were already in, because sorted is stable. That is usually what you want, and when it is not, the fix is a tuple key that names the tiebreak explicitly, such as key=lambda kv: (-kv[1], kv[0]) for "highest count first, then alphabetical".
Comprehensions over dictionaries
The methods on this page and dictionary comprehensions cover the same ground from two directions, and knowing which reads better saves a lot of loops.
A comprehension is the right tool when you are building a new dictionary from an old one: filtering out entries below a threshold, transforming every value, swapping keys and values, or building a lookup table from a list of records. {k: v for k, v in d.items() if v > 0} is one line, and the alternative is three lines with an accumulator.
The methods are the right tool when you are updating a dictionary in place, or when the operation has a name — update, setdefault, pop. Rebuilding a whole dictionary with a comprehension in order to change one entry is wasteful and reads as though something more complicated is happening.
The case where people reach for the wrong one is counting and grouping. A comprehension cannot accumulate, because each iteration produces an independent entry with no access to what came before. That is exactly why Counter, setdefault and defaultdict exist, and why a grouping loop stays a loop.
Questions people ask
Is d.get(k) the same as d[k] with a try? Effectively yes, and get is clearer. Use brackets when a missing key means a bug.
Why did my loop raise "dictionary changed size during iteration"? Because you added or removed a key while iterating a live view. Iterate list(d) instead.
What is the difference between update and |? update modifies in place and returns None; | returns a new dictionary and leaves both operands alone.
Are dictionaries sorted? No. They keep insertion order, which is not the same thing. sorted(d.items()) when you want sorted.
How do I invert a dictionary? {v: k for k, v in d.items()}, remembering that duplicate values collapse — the last one wins.
Is defaultdict always better than setdefault? No. defaultdict creates an entry on any missing lookup, including a typo, which can hide bugs. Use it when every access should create a default.
How do I get the first key? next(iter(d)). There is no indexing, because a dictionary is not a sequence.
Can I use + to merge two dictionaries? No. Use | on 3.9 and later, {**a, **b} before that, or update for in-place.
Does pop work without a key? popitem() removes and returns the last inserted pair, which is useful for draining a dictionary in a loop.
How do I count without importing Counter? d[k] = d.get(k, 0) + 1 in a loop. Counter is faster and clearer, but the one-liner is worth knowing for when the import is not worth it.
What happens if I use an unhashable key by mistake? You get TypeError: unhashable type: 'list' at the moment of insertion, which names the type and is one of the clearer error messages in the language.
Is there a frozen dictionary? Not in the standard library. types.MappingProxyType(d) gives a read-only view of one, which covers most of the reasons people want it.
Recap in one screen
get reads with a fallback, setdefault reads and inserts, defaultdict inserts on every miss — pick by what should happen to the dictionary.items() is the loop you usually want; keys(), values() and items() are live views, not snapshots.- Iterate
list(d) whenever the loop body might change the dictionary. - Keys must be hashable, which means immutable all the way down; tuples are the standard multi-part key.
- A dictionary of functions replaces a chain of
elif that is testing one value against constants.