All 24 modules are open from the start — nothing here is
locked, and nothing costs anything. Sign in so your progress, titles and
credentials stay with you, on every device you use.
Free forever, with your Google account. No password, no payment.
A list answers "what is third?". A dictionary answers "what is the
price of the whisk?" — and it answers it just as fast whether there are ten
entries or ten million. Almost every piece of structured data you will meet at
work, from a JSON API response to a config file to a row of a database, arrives
in this shape.
Ready?
1
A Dictionary Maps Keys to Values
Curly braces, and pairs written key: value. You look things
up by key rather than by position, so the order you wrote them in is
rarely something you care about.
Keys are usually strings and must be something unchangeable — a string, a
number, or a tuple from the next module. A list cannot be a key, for a
reason that will make sense the moment you have met both.
Values can be anything at all, including other dictionaries and lists.
That is what lets a dictionary describe a whole record rather than a
single fact.
A list of records is the usual shape
Not one dictionary — a list of them, one per row. A CSV read into
Python, a JSON array from an API and a database query result all come
out looking like [{...}, {...}, {...}], and that shape is
worth recognising now.
Quick check
Why look something up by key rather than by searching a list for it?
2
[] Raises, .get() Does Not
Asking for a key that is not there with square brackets raises a
KeyError. Often that is exactly what you want — a missing
price is a bug, and a loud failure beats a quiet zero.
When a missing key is expected, .get() hands back
None instead of raising, or whatever default you name:
Choose deliberately. .get() everywhere turns a missing field
into a None that travels three functions before failing
somewhere unrelated; [] everywhere turns an optional field
into a crash. The question is whether absence is normal here.
Changing a dictionary needs no special method — assign to a key and it is
created or replaced:
item["price"] = 1090 # replaces
item["colour"] = "green" # creates
del item["colour"] # removes, KeyError if absent
item.pop("colour", None) # removes and hands back, or the default
And in checks keys, not values.
"price" in item is True;
990 in item is False, even though 990 is
sitting right there as a value.
Quick check
A config may or may not set timeout, and 30 is a sensible default. What reads best?
3
Looping, and the Counting Pattern
Three ways to walk a dictionary, and the third is the one you want most
of the time:
for key in counts: # the keys
for value in counts.values(): # the values
for key, value in counts.items(): # both, which is usually the point
print(key, value)
Looping over the dictionary itself gives keys, not pairs — a small thing
that trips everyone up once.
The pattern worth memorising is counting. In the log
module you kept three separate variables for three levels, which works
until a fourth level appears and does not exist in your code:
counts = {}
for level in levels:
counts[level] = counts.get(level, 0) + 1
One line, and it handles a level it has never seen before:
.get(level, 0) is 0 the first time and the running total
afterwards. Without the default, the first occurrence of anything raises
KeyError.
The same shape totals rather than counts —
totals[key] = totals.get(key, 0) + amount — and that pair
covers a surprising proportion of all reporting code.
Quick check
Why counts.get(level, 0) + 1 rather than counts[level] + 1?
4
The Shapes Real Data Arrives In
Merging. Configuration is nearly always defaults with
overrides on top, and update() is exactly that — it writes
every pair from one dictionary into another, replacing what is already
there:
The .copy() matters for the reason it mattered with lists:
dictionaries are mutable too, and updating DEFAULTS in place
would quietly change the defaults for everything else in the program.
Nesting. A value can be another dictionary, so real
records go two or three levels deep. You read them one step at a time:
This is what a JSON response is. Nothing new is happening — each step is
an ordinary lookup, and the answer to one is the thing the next asks.
Deep lookups fail in the middle
order["customer"]["city"] raises if
customer is missing, and raises differently if
city is. Chained .get() calls help —
order.get("customer", {}).get("city") — at the cost of
turning a structural problem into a None.
Guard at the boundary where the data arrives, so the code below it can
trust the shape.
Quick check
Why DEFAULTS.copy() before update()?
0 of 9 completed
Real Python runs right here in your browser — nothing to install, nothing
sent to a server. The interpreter downloads once the first time you press
Run, then stays cached.
01
To do
Curly braces, pairs written key: value. You look things up by
key rather than by counting positions, which is why a record is a
dictionary and not a list.
Your task: run it and read the KeyError,
then replace that lookup so a missing colour reads as
unspecified. Leave the two lookups that should succeed as
they are.
whisk 990 unspecified
your_code.py
PythonCtrl↵ to run
Hint
item.get("colour", "unspecified") — the second argument is what comes back when the key is not there.
Output
03
To do
No special method for adding. Assign to a key and it is created if it is
new, replaced if it is not.
item["price"] = 1090 # replaces item["colour"] = "green" # creates del item["sku"] # removes old = item.pop("colour") # removes and hands it back
Your task: put the price up to 1090, add
in_stock as True, take the sku out with
del, and pop the colour into removed. Then
print the record and what you popped:
{'name': 'whisk', 'price': 1090, 'in_stock': True} green
your_code.py
PythonCtrl↵ to run
Hint
Assignment for the first two, del for the sku, and removed = item.pop("colour") for the last. pop is the only one that hands something back.
Output
04
To do
"price" in item is True.
990 in item is False — even though 990 is
sitting right there as a value.
This catches people because in on a list checks the items.
On a dictionary the items are the keys, as far as
in is concerned. To ask about a value, ask
item.values().
Your task: print four answers, in this order: is there a
price key, is there a colour key, is
990 a key, is 990 among the values:
True False False True
your_code.py
PythonCtrl↵ to run
Hint
The first three are plain `in item`. The last one is `990 in item.values()`.
Output
05
To do
Looping over a dictionary gives you the keys, not the
pairs. When you want both — which is most of the time —
.items() hands them over two at a time:
for key, value in stock.items(): print(key, value)
Wrapping it in sorted() walks the keys in order, which is
what you want whenever a person is going to read the output.
Your task: print one row per item, keys in alphabetical
order, with the name left-aligned in ten characters and the count
right-aligned in three:
cloth 5 matcha 1 tea 3 whisk 12
your_code.py
PythonCtrl↵ to run
Hint
for name, count in sorted(stock.items()): then print(f"{name:<10}{count:>3}") inside.
Output
06
To do
In the log module you kept a separate variable per level. That works until
a level you did not plan for turns up — and then it is not counted at
all, because there is no variable for it.
A dictionary handles a key it has never seen, in one line:
counts[level] = counts.get(level, 0) + 1
.get(level, 0) is 0 the first time and the running total
after that. Written as counts[level] + 1 it raises
KeyError on the first occurrence of everything.
Your task: tally the levels — including
DEBUG, which nobody planned for — then print the dictionary
and the number of distinct levels:
{'INFO': 3, 'ERROR': 2, 'WARN': 1, 'DEBUG': 1} 4
your_code.py
PythonCtrl↵ to run
Hint
for level in levels: then counts[level] = counts.get(level, 0) + 1. The default is what stops the first sighting of DEBUG from raising.
Output
07
To do
Change the + 1 to + amount and the counting
pattern becomes a totalling one. Between them these two cover a
surprising share of all reporting code.
totals[key] = totals.get(key, 0) + amount
.values() then gives you everything you need to summarise
the result — it works with sum(), max() and the
rest exactly as a list would.
Your task: each entry is a region and an amount. Total by
region, then print the totals, the grand total, and the largest single
region:
{'eu': 1750, 'us': 900, 'jp': 1200} 3850 1750
your_code.py
PythonCtrl↵ to run
Hint
for entry in sales: region, amount = entry.split(":") — then totals[region] = totals.get(region, 0) + int(amount). The amount arrives as text.
Output
08
To do
Almost every configuration is this: a set of defaults, and the handful of
things one particular deployment does differently.
update() writes every pair from one dictionary into another,
replacing what is already there.
The .copy() matters for the same reason it did with lists.
update() changes the dictionary in place, so without it you
have overwritten the defaults for the rest of the program.
Your task: merge the two, leaving DEFAULTS
exactly as it was, and print the result and then the untouched defaults:
Two steps: settings = DEFAULTS.copy(), then settings.update(user_config). update() returns None, so do not assign its result.
Output
09
To do
A value can be another dictionary, or a list. That is what a JSON
response is, and reading one is just an ordinary lookup whose answer is
the thing the next lookup asks.
order["customer"]["city"] order["items"][0]
Your task: pull four things out of the order — the
customer's name, their city, the first item, and how many items there are
— and print them:
Kenji Osaka tea 2
your_code.py
PythonCtrl↵ to run
Hint
One step at a time: order["customer"] is a dictionary, so ["name"] on the end of that gets the name. order["items"] is a list, so [0] and len() work on it.
Output
The support inbox
To do
Every morning the support tool exports the previous day's tickets and
somebody summarises them. Read the export once and produce the summary.
Each real line is ticket id,priority,minutes spent. Blank lines
and lines starting with # are not tickets.
Build these:
counts — how many tickets at each priority
minutes — total minutes spent at each priority
busiest — the priority with the most tickets
slowest_id and slowest_minutes — the single ticket that took longest
Do not assume which priorities exist. The export gains a new one the first
time someone adds a level in the admin panel, and a summary that quietly
drops it is worse than one that crashes.
Then print exactly six lines — one per priority in alphabetical order, with the name left-aligned in eight, the count right-aligned in two, and the minutes right-aligned in six followed by m:
Tickets: 6 high 3 150m low 2 15m medium 1 30m Busiest: high Slowest: T-1003 (90m)
your_code.py
PythonCtrl↵ to run
Hint
Loop over export.splitlines(), strip, and continue past blanks and # lines. Split the rest on the comma into three pieces; the minutes need int(). Tally with counts[priority] = counts.get(priority, 0) + 1 and minutes[priority] = minutes.get(priority, 0) + spent. Track the slowest ticket with a comparison as you go. For busiest, loop over sorted(counts) afterwards and keep the largest. The rows are f"{name:<8}{count:>2}{mins:>6}m".