Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 15 of 898 min

Dictionary patterns: defaults, counting and grouping

The KeyError, and the two ways round it

python
prices = {"rice": 340, "dal": 1250}
prices["oil"]              # KeyError: 'oil'
prices.get("oil")          # None
prices.get("oil", 0)       # 0

get never raises. It returns None, or whatever default you give it. That makes it right for reading data you did not create — an API response, a CSV row, a config file — where a missing key is expected rather than exceptional.

prices["oil"] is right when a missing key means the program is wrong and should stop. Choosing deliberately between the two is a real design decision: get with a default silently continues on data you may have misunderstood, and a KeyError at the moment of the mistake is often more useful than a None that surfaces four functions later as TypeError: unsupported operand.

Counting

The pattern everybody writes first:

python
counts = {}
for word in words:
    if word in counts:
        counts[word] += 1
    else:
        counts[word] = 1

Two shorter forms do the same thing:

python
for word in words:
    counts[word] = counts.get(word, 0) + 1
python
from collections import Counter
counts = Counter(words)
counts.most_common(5)      # the five most frequent, as (word, count) pairs

Counter is part of the standard library — nothing to install. It is a dictionary that returns 0 for missing keys instead of raising, and most_common alone justifies knowing it. Counting word frequencies, error types in a log, or labels in a dataset is three lines.

Grouping

Collecting items under a key is the same shape:

python
from collections import defaultdict
by_city = defaultdict(list)
for person in people:
    by_city[person["city"]].append(person["name"])

defaultdict(list) creates an empty list the first time each key is touched, so the if key not in d line disappears. Without it, setdefault does the same job in one expression:

python
by_city.setdefault(person["city"], []).append(person["name"])

One caution about defaultdict: merely reading a missing key creates it. print(by_city["Pune"]) on an unknown city prints [] and permanently adds Pune to the dictionary. If that matters, call dict(by_city) when you have finished building, which gives back an ordinary dictionary that raises properly.

Walking a dictionary

python
for key in prices:                  # keys — the default
for value in prices.values():
for key, value in prices.items():   # the one you usually want

Looping over a dictionary gives keys, not pairs. .items() gives both, and unpacks into two names. Since Python 3.7 the order is guaranteed to be the order keys were first inserted, which means a dictionary built from a CSV keeps the column order of the file — useful, and safe to rely on, unlike set order.

Merging

python
defaults = {"model": "small", "retries": 3}
user = {"retries": 5}
config = defaults | user        # Python 3.9+
config = {**defaults, **user}   # any version

The right-hand side wins on conflicts. Both build a new dictionary. defaults.update(user) modifies defaults in place, which is fine until defaults is a module-level constant that other code also reads, at which point you have changed a shared value for the rest of the run.

Inverting, and what it loses

python
codes = {"rice": "R01", "dal": "D01"}
by_code = {v: k for k, v in codes.items()}

If two keys shared a value, the inverted dictionary keeps only the last one, silently. Check len(by_code) == len(codes) before trusting it. That one-line assertion has saved a great many afternoons.

Comparing two dictionaries

Keys are sets, so you can subtract them:

python
missing = old.keys() - new.keys()      # keys that disappeared
added   = new.keys() - old.keys()
changed = {k for k in old.keys() & new.keys() if old[k] != new[k]}

.keys() returns a view that supports set operations directly. This is the shortest honest way to diff two configurations or two API responses.

Deleting

python
del prices["oil"]              # KeyError if absent
prices.pop("oil", None)        # removes if present, returns the default if not

pop with a default is the safe removal, and it hands you the value on the way out, which del does not.

The views are live

.keys(), .values() and .items() are views, not copies. They reflect later changes to the dictionary, which is efficient and occasionally surprising:

python
ks = prices.keys()
prices["ghee"] = 600
len(ks)     # already includes ghee

And because they are live, adding or deleting keys while looping over a view raises RuntimeError: dictionary changed size during iteration. To delete while iterating, loop over list(prices.keys()), which is a snapshot.

The one thing to keep

`get` with a default and `defaultdict` remove the missing-key branch, but both hide data you may have misread, so use plain `d[key]` when absence means the program is wrong.

Before you move on

A script builds `by_city = defaultdict(list)`, fills it, then logs `print(len(by_city))` after a debugging line that printed `by_city["Kochi"]` for a city with no records. The count is one higher than the number of cities with data. Why?

Pick the one you would defend. Nobody sees your answer.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly