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.
Python ships with a library that covers dates, randomness, statistics,
JSON, file paths, regular expressions and a great deal more. Knowing what is in
it is the difference between forty lines and one — and the forty-line version is
usually the one with the bug in it, because the standard library's has been
tested by rather more people than you.
Ready?
1
Three Ways to Bring Something In
import math # the whole module
math.sqrt(144)
from math import sqrt, ceil # just these names
sqrt(144)
import statistics as stats # under a shorter name
stats.mean(scores)
The first form keeps the module's name in front of everything, which is
usually what you want: math.sqrt says where it came from, and
two modules can both have a sqrt without any argument about
it.
from is right when a name is used constantly and is
unmistakable — from datetime import date, and every line
below is shorter for it. It is wrong when the bare name could be anything:
from json import load gives you a load that
tells the next reader nothing.
The alias form is for long names, and by convention only where the whole
community uses the same abbreviation. Inventing your own is how code
becomes unreadable to everyone but its author.
Never from x import *
It pulls in every public name the module has, so you no longer know
what is defined where — and a later import can silently replace a name
an earlier one gave you. The error, when it comes, is a function
behaving oddly rather than anything that names the import.
Quick check
Which import reads best for a name used once, halfway down a long file?
2
The Ones Worth Knowing by Name
math
sqrt, ceil, floor, pi. ceil and floor always round the same direction, unlike round().
statistics
mean, median, stdev. The median is the one people mean when they say average and get a wrong answer.
random
randint, choice, shuffle, sample. seed() makes it repeatable, which is how you test anything that uses it.
datetime
date, timedelta. Subtracting two dates gives a duration; adding a duration gives a date. Never do calendar arithmetic by hand.
json
loads for text to Python, dumps for the other way. The s means "string" — the versions without it work on files.
collections
Counter tallies in one line; defaultdict removes the "is this key there yet" check.
Counter is worth dwelling on, because it replaces something
you have now written twice:
counts = Counter(levels) # the whole get(key, 0) + 1 loop
counts.most_common(3) # the three commonest, with their counts
And json.loads turns text into exactly the dictionaries and
lists from Part 2 — which is why that module spent so long on nested
lookups.
Quick check
How many days between two dates?
3
Your Own Code Is a Module Too
Any .py file is a module. Put functions in
pricing.py and another file can say:
import pricing
pricing.line_total(3, 1250)
from pricing import line_total
line_total(3, 1250)
Importing a module runs it, once. Every function
definition happens, and so does every line that is not inside one — which
is why a file that prints a report at the top level will print it the
moment anyone imports it.
The guard that stops that is worth recognising on sight:
def main():
...
if __name__ == "__main__":
main()
__name__ is "__main__" when the file is the one
being run, and the module's own name when it is being imported. So the
block runs when you execute the file and stays quiet when somebody
imports it — the same file usable as a tool and as a library.
One idea per module
The same rule as functions, one level up. pricing.py,
parsing.py, report.py — each one nameable in
a phrase. A file called utils.py is where code goes when
nobody decided what it was, and it grows until nobody can say what it
contains.
Imports go at the top of the file, standard library first, in one
block. Not scattered through the body where the reader has to go
hunting.
Quick check
What does if __name__ == "__main__": achieve?
4
Three Ways an Import Goes Wrong
Shadowing a module with a file. Name your own file
random.py and every import random in that
directory finds yours instead of the standard library's. The error is
AttributeError: module 'random' has no attribute 'randint',
which points at the wrong thing entirely.
The same happens with a variable. Once you write
math = 5, the name math is that number, and
math.sqrt is an AttributeError on an integer.
Importing what you did not mean.import datetime gives you the module;
from datetime import datetime gives you the
class inside it, which happens to have the same name. Both are
common, and mixing them up produces
TypeError: 'module' object is not callable.
Circular imports. Two modules that import each other.
Python is part way through defining the first when it starts on the
second, which asks for something from the first that does not exist yet.
The fix is never a clever import — it is noticing that the two files want
to be one, or that a third one should hold what they share.
AttributeError
math = 5 math.sqrt(9)
The name is a number now. The module is still loaded and no longer reachable.
Fine
max_value = 5 math.sqrt(9)
Never name a variable after a module you are using.
Quick check
A file in your project is called json.py. What happens to import json elsewhere in that directory?
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
import math makes the module available under its own name,
so every use says where it came from.
ceil and floor always round the same direction,
which is what you want when the question is "how many boxes" rather than
"what is the nearest number".
Your task: print the square root of 144, the ceiling of
4.1, the floor of 4.9, and pi to four decimal places:
12.0 5 4 3.1416
your_code.py
PythonCtrl↵ to run
Hint
import math at the top. The last line is round(math.pi, 4) — or an f-string with a .4f spec, which would give the same characters here.
Output
02
To do
from math import ceil, floor brings those two names into
your file directly, so calls are shorter.
This reads well when the name is unmistakable and used constantly. It
reads badly when the bare name could have come from anywhere — a lone
load() tells the next reader nothing.
Your task: a warehouse packs items into boxes of 12.
Import ceil and floor directly and work out how
many boxes are needed for 100 items, and how many of those are full:
9 8
Nine boxes are needed because the last four items still need one; eight
of them are full.
your_code.py
PythonCtrl↵ to run
Hint
from math import ceil, floor at the top. ceil rounds up for the boxes needed, floor rounds down for the full ones.
Output
03
To do
import statistics as stats brings the module in under a name
of your choosing.
Worth doing for genuinely long module names, and worth doing only with
the abbreviation everyone else uses — an alias nobody recognises is
strictly worse than the full name.
mean and median are not the same question. Five
salaries of 30, 31, 32, 33 and 200 have a mean of 65 and a median of 32,
and only one of those describes what most people earn.
Your task: import statistics under the alias
stats and print the mean and median of the scores, then of
the salaries:
76.25 79.5 65.2 32
your_code.py
PythonCtrl↵ to run
Hint
import statistics as stats, then stats.mean(...) and stats.median(...). Notice how far apart the two salary figures are.
Output
04
To do
random gives you dice rolls, shuffles and samples. It also
gives you code that behaves differently every run, which is impossible to
test and unpleasant to debug.
random.seed(n) fixes the starting point, so the same seed
produces the same sequence every time. In real code you seed in the test
and leave it alone in production.
Your task: seed with 7, then print five dice
rolls as a list and a shuffled copy of the queue. With that seed the
answers are fixed:
Seed once at the top, before either call — the rolls and the shuffle draw
from the same sequence.
your_code.py
PythonCtrl↵ to run
Hint
random.seed(7) first. rolls is a comprehension over range(5) calling random.randint(1, 6). random.shuffle(queue) changes the list in place and returns None, so call it on its own line.
Output
05
To do
Months have different lengths, years have different lengths, and every
piece of code that assumes otherwise fails eventually in a way nobody
traces back.
from datetime import date, timedelta d = date(2026, 9, 2) d + timedelta(days=30) # a date (later - earlier).days # a whole number of days
Subtracting two dates gives a duration; .days takes the
number out of it. Adding a timedelta gives a date back.
Your task: from 2 September 2026, print the date itself,
the date thirty days later, the number of days until Christmas, and the
start date formatted as day, short month, year:
2026-09-02 2026-10-02 114 02 Sep 2026
your_code.py
PythonCtrl↵ to run
Hint
from datetime import date, timedelta. date(2026, 9, 2) builds the start and printing it gives the ISO form. The gap is (christmas - start).days, and the last line is start.strftime("%d %b %Y").
Output
06
To do
A JSON response is text. json.loads turns it into exactly
the dictionaries and lists from Part 2, which is why that module spent
so long on nested lookups.
obj = json.loads(raw) # text -> Python text = json.dumps(obj) # Python -> text
The s means "string". The versions without it read from and
write to files, which is the next module.
sort_keys=True on dumps makes the output
stable, which matters the moment two runs are compared to each other.
Your task: parse the response, print the quantity, the
first tag, and how many tags there are, then print the record back out
as text with sorted keys:
3 hot 2 {"name": "tea", "qty": 3, "tags": ["hot", "green"]}
your_code.py
PythonCtrl↵ to run
Hint
import json, then record = json.loads(raw). After that it is ordinary dictionary and list work. The last line is json.dumps(record, sort_keys=True).
Output
07
To do
Counter does the whole
counts[key] = counts.get(key, 0) + 1 loop from module 2-05,
and adds a way to ask for the commonest entries.
It behaves like a dictionary everywhere it matters, with one useful
difference: a key it has never seen reads as 0 rather than
raising.
Your task: tally the levels, then print the commonest
two, the count of INFO, the count of a level that never
appeared, and the number of distinct levels:
[('INFO', 3), ('ERROR', 2)] 3 0 3
your_code.py
PythonCtrl↵ to run
Hint
from collections import Counter, then counts = Counter(levels). most_common(2) takes an argument for how many you want, and counts["DEBUG"] is 0 rather than a KeyError.
Output
08
To do
Collecting things into per-key lists needs a check every time: is there a
list under this key yet? defaultdict answers it once, at
creation.
from collections import defaultdict groups = defaultdict(list) groups["eu"].append("tea") # the list is created on demand
You hand it the function that makes a default —
list, not list(). It calls it when a missing key
is first touched.
Your task: group the items by region, then print the
groups as an ordinary dictionary, one region's list, and the number of
regions:
groups = defaultdict(list) — pass the type itself, with no brackets. Then groups[region].append(item) works even the first time a region appears.
Output
09
To do
import math binds the name math to the module.
Assign anything else to that name and the module is still loaded and no
longer reachable through it.
The error —
AttributeError: 'int' object has no attribute 'sqrt' —
names the type rather than the mistake, which is why this can take longer
to spot than it deserves. The same thing happens at file level: a file of
your own called random.py makes the real one unreachable
from anywhere in that directory.
Your task: run it, read the error, then rename the
variable — leaving the import and the calculation alone — so it prints:
12.0 5
your_code.py
PythonCtrl↵ to run
Hint
The variable wants a name of its own. Call it something meaningful — per_box, say — and the two prints then need that name in the second one.
Output
The daily digest
To do
Every night the platform posts a JSON summary of the day's sessions, and
every morning somebody turns it into four lines that fit in a chat message.
Write that.
The payload is JSON text: a date, and a list of sessions each with a
learner, a track and a number of minutes.
Use the standard library for all of it. Every figure below has a
one-call answer, and a hand-rolled version of any of them fails a check.
payload — the parsed JSON
reported_on — the payload's date, as a real date object
by_track — a Counter of sessions per track
minutes_by_learner — a defaultdict(list) of every learner's session lengths
median_minutes — the median session length across every session
busiest — the track with the most sessions, as a plain string
Then print exactly four lines:
Digest for 02 Sep 2026 Sessions: 6 across 3 tracks Busiest: python (3) Median session: 35 min, longest learner total: 120
The date is formatted day, short month, year. The last figure is the
highest total minutes any single learner accumulated.
your_code.py
PythonCtrl↵ to run
Hint
json.loads for the payload. date.fromisoformat(payload["date"]) gives a real date, and .strftime("%d %b %Y") formats it. Counter takes a list of every session's track — build that with a comprehension. defaultdict(list) and one loop for the learner totals. statistics.median over every session's minutes. by_track.most_common(1)[0] gives the busiest track and its count as a tuple. The longest learner total is max() over the sums of each learner's list.