Session 22 of the Python Fundamentals series: the "batteries included" tour. It is a guided, runnable walk through the modules you reach for daily - math, with sqrt, floor, ceil, and pi; random, with randint, choice, shuffle, and seed for reproducibility; datetime, with date, timedelta, and strftime; pathlib.Path, with the / operator, name, suffix, and stem, and exists; and collections, with Counter and defaultdict - plus a look at os and sys. The traps are calling a function before you import its module, reinventing something the library already ships, and using random without a seed, so that results can never be reproduced. Every snippet and error message was executed and copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 99 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 22
Batteries included: reach for the library before you write it yourself
Objectives
Python ships with a huge toolbox already installed. This session is a guided tour of the modules you will use every week. By the end you can:
import a standard-library module and call its functions.math for sqrt, floor, ceil, and pi.random and set a seed so results are reproducible.date, timedelta, and strftime.pathlib.Path.Counter and defaultdict, and know when to reach for the library instead of rolling your own.Warm-up
Discussion prompt
Before we open Session 22 - Standard Library Tour: without looking back, what was the main idea of Session 21 - Modules, Packages & pip, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 21 of the Python Fundamentals series, in depth. Reusing code beyond a single file: importing from the standard library (import math, from math import sqrt, import x as np, from x import *), writing and importing your own module, the if __name__ == '__main__' guard, packages as folders of modules, installing third-party code with pip and requirements.txt, isolating projects with virtual environments, and the import search path.
Section
Part 1
Concept
The standard library is a large set of ready-made modules installed alongside Python itself. No download, no pip - it is already on your machine.
standard library — The collection of modules bundled with Python. 'Batteries included' - common jobs like math, dates, files, and randomness already have tested tools.
Counterexample
Discussion prompt
The standard library is a large set of ready-made modules installed alongside Python itself. No download, no pip - it is already on your machine.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
A module sits idle until you import it. After import math, you reach its tools through the module name: math.sqrt(...).
Put your imports at the top of the file so every line below can use them.
Analogy
Discussion prompt
Explain import brings a module in by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
A module sits idle until you import it. After import math, you reach its tools through the module name: math.sqrt(...).
Concept
import math brings in the whole module; you then write math.sqrt. from datetime import date pulls one name in so you write date directly.
| you write | then you call | reads as |
|---|---|---|
| import math | math.sqrt(9) | sqrt, from math |
| from datetime import date | date(2026, 7, 15) | date, imported by name |
Comparison
Comparison matrix
From Two ways to import: refill the then you call column from what you know. The rest of the table is as it appeared.
| you write | then you call | reads as |
|---|---|---|
| import math | math.sqrt(9) | sqrt, from math |
| from datetime import date | date(2026, 7, 15) | date, imported by name |
Worked example
import math
print(math.sqrt(144))
print(math.floor(3.7))Line 1 loads the math module
Why: Now the name math points at the whole toolbox of math functions.
Call tools through the module name
Why: Verified by execution: math.sqrt(144) is 12.0 and math.floor(3.7) is 3.
| call | returns |
|---|---|
| math.sqrt(144) | 12.0 |
| math.floor(3.7) | 3 |
Trade off
Comparison matrix
From Import and use a module: every row here is a choice with a cost. Fill the returns column, then say which row you would actually pick and what you give up for it.
| call | returns |
|---|---|
| math.sqrt(144) | 12.0 |
| math.floor(3.7) | 3 |
Anomaly
Predict first
A student writes this, and it looks reasonable:
You call a function without importing its module first.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: sqrt lives inside math, not in the plain language.
Import the module, then reach the tool through it.
Why: sqrt lives inside math, not in the plain language. With no import, the name is undefined and Python raises NameError.
Trap
You call a function without importing its module first.
sqrt(16)Python does not know the bare name sqrt
Why: sqrt lives inside math, not in the plain language. With no import, the name is undefined and Python raises NameError.
| you write | result |
|---|---|
| sqrt(16) | NameError: name 'sqrt' is not defined |
Import the module, then reach the tool through it.
import math
print(math.sqrt(16))math.sqrt is found and runs
Why: Verified by execution: prints 4.0. The name lives on the module, so you say math.sqrt, not sqrt.
| you write | prints |
|---|---|
| math.sqrt(16) | 4.0 |
Error analysis
Annotate
Walk the callouts on Trap: using a tool before importing it. Each one is a place this is easy to get subtly wrong.
Section
Part 2
Concept
The math module gives you square roots, rounding to whole numbers, constants like pi, and more - all tested and fast.
floor rounds down to the nearest whole number; ceil rounds up. Both return an int.
Explain it
Discussion prompt
Explain math handles the tricky arithmetic to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
The math module gives you square roots, rounding to whole numbers, constants like pi, and more - all tested and fast.
Pattern
Predict first
The table runs: math.sqrt(2) | 1.4142135623730951 · math.floor(3.7) | 3
In sqrt, floor, and ceil, given the rows so far: what is the next one — the row where call is math.ceil(3.2)?
Correct: math.ceil(3.2) | 4
| call | returns |
|---|---|
| math.sqrt(2) | 1.4142135623730951 |
| math.floor(3.7) | 3 |
| math.ceil(3.2) | 4 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Verified by execution: math.sqrt(2) is 1.4142135623730951 - a full-precision float.
Worked example
import math
print(math.sqrt(2))
print(math.floor(3.7))
print(math.ceil(3.2))sqrt gives a float
Why: Verified by execution: math.sqrt(2) is 1.4142135623730951 - a full-precision float.
floor rounds down, ceil rounds up
Why: Verified by execution: floor(3.7) is 3, ceil(3.2) is 4 - never the nearest, always the direction named.
| call | returns |
|---|---|
| math.sqrt(2) | 1.4142135623730951 |
| math.floor(3.7) | 3 |
| math.ceil(3.2) | 4 |
Blank canvas
Draw it
Draw what sqrt, floor, and ceil just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
'Down' means toward negative infinity, not toward zero. So floor(-2.1) is -3, and ceil(-2.1) is -2.
This trips people up - down on the number line, not down in size.
Worked example
import math
print(math.floor(-2.1))
print(math.ceil(-2.1))floor moves left on the number line
Why: Verified by execution: floor(-2.1) is -3 (the whole number just below -2.1).
ceil moves right
Why: Verified by execution: ceil(-2.1) is -2 (the whole number just above -2.1).
| call | returns |
|---|---|
| math.floor(-2.1) | -3 |
| math.ceil(-2.1) | -2 |
Worked example
import math
r = 3
print(round(math.pi * r ** 2, 2))
print(round(2 * math.pi * r, 2))math.pi is the constant 3.141592653589793
Why: You never type pi's digits - the module holds it to full precision.
Area then circumference, rounded
Why: Verified by execution: pirr is 28.27 and 2pir is 18.85 after round(..., 2).
| formula | value | rounded |
|---|---|---|
| math.pi | 3.141592653589793 | - |
| pi * 3 ** 2 | 28.274... | 28.27 |
| 2 * pi * 3 | 18.849... | 18.85 |
Intuition
Every function in math was written, tested, and optimized by experts. Typing math.sqrt(2) borrows all of that work for free.
The habit to build: when a job feels common - roots, dates, randomness, counting - assume the library already has it, and go look.
Section
Part 3
Concept
The random module produces pseudo-random results: randint(a, b) for a whole number in a range, choice(seq) to pick an item, shuffle(list) to reorder in place.
randint(a, b) — A random integer from a to b, including both ends. randint(1, 6) is a fair six-sided die.
Definition probe
Sort into buckets
Every line below is part of the definition of standard library or of randint(a, b) — one or the other, never both. Put each where it belongs.
Concept
random.seed(n) fixes the starting point of the sequence. Same seed, same numbers, every run - essential for tests and for demos you want to repeat.
Every example here calls seed first so the output is identical on your machine.
Worked example
import random
random.seed(42)
print(random.randint(1, 6))
print(random.randint(1, 6))
print(random.randint(1, 6))seed(42) fixes the sequence
Why: From this seed the rolls are fully determined - anyone running it sees the same three numbers.
Three rolls come out in order
Why: Verified by execution: 6, then 1, then 1.
| call | roll |
|---|---|
| first randint(1, 6) | 6 |
| second randint(1, 6) | 1 |
| third randint(1, 6) | 1 |
Sorting
Sort into buckets
These are the pieces of Session 22 - Standard Library Tour, out of order. Put each one back under the part of the lesson it belongs to.
Fill the middle
Fill in the blanks
From choice and shuffle — one line has had its right-hand side removed. Put it back.
import random
random.seed(7)
deck = [1, 2, 3, 4, 5]
random.shuffle(deck)
print(deck)
Why: deck is what everything below it consumes, so the wrong expression here fails later and somewhere else. It changes deck itself and returns None - so you print deck, not the call.
Worked example
import random
random.seed(7)
deck = [1, 2, 3, 4, 5]
random.shuffle(deck)
print(deck)shuffle reorders the list in place
Why: It changes deck itself and returns None - so you print deck, not the call.
Read the shuffled order
Why: Verified by execution: [5, 1, 4, 2, 3] for seed 7.
| step | deck |
|---|---|
| before shuffle | [1, 2, 3, 4, 5] |
| after shuffle | [5, 1, 4, 2, 3] |
Worked example
import random
random.seed(0)
colors = ["red", "green", "blue"]
print(random.choice(colors))
print(random.choice(colors))choice returns one element
Why: It reaches into the list and hands back a single item, leaving the list unchanged.
Read both picks
Why: Verified by execution: green, then green - for seed 0 both picks land on the same item.
| call | returns |
|---|---|
| first choice(colors) | green |
| second choice(colors) | green |
Anomaly
Predict first
A student writes this, and it looks reasonable:
No seed - so the same code gives different numbers each run, and a bug you saw once may never come back.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: With no fixed starting point, you cannot reproduce a result.
Seed first - same seed, same sequence, every time.
Why: With no fixed starting point, you cannot reproduce a result. There is no single 'correct' output to trace - that is the whole problem.
Trap
No seed - so the same code gives different numbers each run, and a bug you saw once may never come back.
import random
print(random.randint(1, 100))
# run again -> a different numberEvery run diverges
Why: With no fixed starting point, you cannot reproduce a result. There is no single 'correct' output to trace - that is the whole problem.
| run | output |
|---|---|
| first run | unpredictable |
| second run | usually different |
Seed first - same seed, same sequence, every time.
import random
random.seed(1)
print(random.randint(1, 100))
random.seed(1)
print(random.randint(1, 100))Reset to seed 1 and the value repeats
Why: Verified by execution: 18, then 18. Re-seeding rewinds the sequence to the same spot.
| step | prints |
|---|---|
| seed(1); randint | 18 |
| seed(1); randint | 18 |
Break the constraint
Discussion prompt
The rule this trap just fixed:
Verified by execution: 18, then 18. Re-seeding rewinds the sequence to the same spot.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
With no fixed starting point, you cannot reproduce a result. There is no single 'correct' output to trace - that is the whole problem.
Section
Part 4
Concept
From the datetime module, date(year, month, day) builds one calendar day. It knows its .year, .month, and .day.
date.today() gives the current day from the system clock - great for real programs, but its value changes daily, so the examples here use a fixed date.
Worked example
from datetime import date
d = date(2026, 7, 15)
print(d)
print(d.year, d.month, d.day)from ... import pulls one name in
Why: from datetime import date lets you write date(...) directly instead of datetime.date(...).
Print the day and its fields
Why: Verified by execution: printing d shows 2026-07-15; the fields are 2026 7 15.
| expression | value |
|---|---|
| d | 2026-07-15 |
| d.year | 2026 |
| d.month | 7 |
| d.day | 15 |
Pattern
Step through it
Step through Build a date and read its parts one row at a time. What is driving the change, and what would the row after the last one be?
Concept
timedelta(days=n) is a length of time you can add to or subtract from a date. Date plus span gives a new date.
Subtract one date from another and you get a timedelta back - ask it .days for the gap.
Faded example
Fill in the blanks
Add days with timedelta, with the scaffolding fading: two lines are gone now — fill both.
from datetime import date, timedelta
d = date(2026, 7, 15)
later = d + timedelta(days=10)
print(later)
Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. timedelta(days=10) is the length; adding it rolls the calendar forward, handling month ends for you.
Worked example
from datetime import date, timedelta
d = date(2026, 7, 15)
later = d + timedelta(days=10)
print(later)Add a 10-day span to the date
Why: timedelta(days=10) is the length; adding it rolls the calendar forward, handling month ends for you.
Read the new date
Why: Verified by execution: 2026-07-25.
| step | value |
|---|---|
| d | 2026-07-15 |
| timedelta(days=10) | 10 days |
| d + timedelta | 2026-07-25 |
Comparison
Comparison matrix
From Add days with timedelta: refill the value column from what you know. The rest of the table is as it appeared.
| step | value |
|---|---|
| d | 2026-07-15 |
| timedelta(days=10) | 10 days |
| d + timedelta | 2026-07-25 |
Faded example
Fill in the blanks
Days between two dates, with the scaffolding fading: two lines are gone now — fill both.
from datetime import date
start = date(2026, 1, 1)
end = date(2026, 7, 15)
gap = end - start
print(gap.days)
Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. end - start is a span; printing it whole shows '195 days, 0:00:00'.
Worked example
from datetime import date
start = date(2026, 1, 1)
end = date(2026, 7, 15)
gap = end - start
print(gap.days)Subtracting dates gives a timedelta
Why: end - start is a span; printing it whole shows '195 days, 0:00:00'.
Ask the span for its days
Why: Verified by execution: gap.days is 195.
| expression | value |
|---|---|
| end - start | 195 days, 0:00:00 |
| gap.days | 195 |
Worked example
from datetime import date
d = date(2026, 7, 15)
print(d.strftime("%Y-%m-%d"))
print(d.strftime("%B %d, %Y"))
print(d.strftime("%A"))strftime turns a date into text
Why: Each %-code is a placeholder: %Y year, %m month, %d day, %B month name, %A weekday name.
Read the three formats
Why: Verified by execution: 2026-07-15, then July 15, 2026, then Wednesday.
| format string | output |
|---|---|
| %Y-%m-%d | 2026-07-15 |
| %B %d, %Y | July 15, 2026 |
| %A | Wednesday |
Pattern
Step through it
Step through Format a date with strftime one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 5
Concept
from pathlib import Path gives you Path, an object that represents a file or folder location and knows how to take itself apart.
Path — A pathlib object standing for a filesystem path. It exposes parts like .name and .suffix and can check .exists() - no manual string slicing.
Worked example
from pathlib import Path
p = Path("reports/summary.txt")
print(p.name)
print(p.suffix)
print(p.stem)Ask the path for its pieces
Why: .name is the final component, .suffix the extension, .stem the name without the extension.
Read the parts
Why: Verified by execution: summary.txt, then .txt, then summary.
| attribute | value |
|---|---|
| p.name | summary.txt |
| p.suffix | .txt |
| p.stem | summary |
Pattern
Step through it
Step through name, suffix, and stem one row at a time. What is driving the change, and what would the row after the last one be?
Concept
Once you have a Path, the / operator joins a folder and a child: Path("data") / "scores.csv". Python inserts the right separator for your operating system.
No more gluing strings with slashes by hand - and no more guessing / versus \.
Worked example
from pathlib import Path
full = Path("data") / "scores.csv"
print(full.name)
print(full.suffix)
print(full.as_posix())/ joins folder and file into one Path
Why: The result is a Path, so it still knows its own .name and .suffix.
Read the joined path
Why: Verified by execution: name is scores.csv, suffix is .csv, and .as_posix() shows data/scores.csv with forward slashes.
| expression | value |
|---|---|
| full.name | scores.csv |
| full.suffix | .csv |
| full.as_posix() | data/scores.csv |
Fill the middle
Fill in the blanks
From Check whether a path exists — one line has had its right-hand side removed. Put it back.
from pathlib import Path
p = Path("notes.txt")
print(p.exists())
missing = Path("ghost.txt")
print(missing.exists())
Why: missing is what everything below it consumes, so the wrong expression here fails later and somewhere else. It returns True if a real file or folder is there, False otherwise - a safe check before you read.
Worked example
from pathlib import Path
p = Path("notes.txt")
print(p.exists())
missing = Path("ghost.txt")
print(missing.exists()).exists() asks the filesystem
Why: It returns True if a real file or folder is there, False otherwise - a safe check before you read.
Read both answers
Why: Verified by execution in a folder holding notes.txt but not ghost.txt: True, then False.
| path | exists() |
|---|---|
| notes.txt (present) | True |
| ghost.txt (absent) | False |
Intuition
"data/scores.csv" is just characters. A Path is an object that understands it is a path - so you ask it questions instead of slicing text and hoping.
That is the pattern across the library: wrap raw data in the right object and the useful operations come along for free.
Section
Part 6
Concept
Counter from collections counts how often each item appears. Hand it a list or string and it returns a dict-like tally.
Ask for a missing key and you get 0, not an error - it assumes anything uncounted has a count of zero.
Explain it
Discussion prompt
Explain Counter tallies for you to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Counter from collections counts how often each item appears. Hand it a list or string and it returns a dict-like tally.
Worked example
from collections import Counter
votes = ["yes", "no", "yes", "yes", "no"]
tally = Counter(votes)
print(tally)
print(tally["yes"])
print(tally["maybe"])Counter walks the list and tallies
Why: One pass builds the counts; printing it shows Counter({'yes': 3, 'no': 2}).
Look up present and absent keys
Why: Verified by execution: tally['yes'] is 3, and a never-seen key like 'maybe' is 0 (no KeyError).
| expression | value |
|---|---|
| tally | Counter({'yes': 3, 'no': 2}) |
| tally['yes'] | 3 |
| tally['maybe'] | 0 |
Worked example
from collections import Counter
words = "the cat the dog the bird".split()
c = Counter(words)
print(c.most_common(2))split makes a list of words
Why: "the cat the dog the bird".split() is ['the', 'cat', 'the', 'dog', 'the', 'bird'].
most_common(2) ranks the top two
Why: Verified by execution: [('the', 3), ('cat', 1)] - most frequent first, as (item, count) pairs.
| word | count |
|---|---|
| the | 3 |
| cat | 1 |
| dog | 1 |
| bird | 1 |
Discrimination
Sort into buckets
Sort these by count, from memory, without looking back at most_common finds the top item. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Concept
A defaultdict(int) starts any missing key at 0; a defaultdict(list) starts it at an empty list. You skip the 'is the key there yet?' check.
defaultdict(factory) — A dict that auto-creates a missing key using factory() - int gives 0, list gives []. The factory is the type you pass in.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
Why: These are the working definitions of standard library, randint(a, b), Path, defaultdict(factory) as Session 22 - Standard Library Tour uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Worked example
from collections import defaultdict
groups = defaultdict(list)
groups["fruit"].append("apple")
groups["fruit"].append("pear")
groups["veg"].append("kale")
print(groups["fruit"])First touch of a key makes an empty list
Why: groups['fruit'] did not exist, so defaultdict created [] - then .append works immediately.
Items collect under each key
Why: Verified by execution: groups['fruit'] is ['apple', 'pear'].
| step | groups['fruit'] |
|---|---|
| append 'apple' | ['apple'] |
| append 'pear' | ['apple', 'pear'] |
Blank canvas
Draw it
Draw what Group items with defaultdict(list) just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Fill the middle
Fill in the blanks
From Count letters with defaultdict(int) — one line has had its right-hand side removed. Put it back.
from collections import defaultdict
counts = defaultdict(int)
for ch in "hello":
counts[ch] += 1
print(dict(counts))
Why: counts is what everything below it consumes, so the wrong expression here fails later and somewhere else. counts[ch] += 1 works even the first time: the missing key defaults to 0 before adding.
Worked example
from collections import defaultdict
counts = defaultdict(int)
for ch in "hello":
counts[ch] += 1
print(dict(counts))Each new letter starts at 0, then +1
Why: counts[ch] += 1 works even the first time: the missing key defaults to 0 before adding.
Trace the counts through the word
Why: Verified by execution: final dict is {'h': 1, 'e': 1, 'l': 2, 'o': 1}.
| char | counts after | note |
|---|---|---|
| h | {h: 1} | new, 0 then +1 |
| e | {h: 1, e: 1} | new |
| l | {h: 1, e: 1, l: 1} | new |
| l | {h: 1, e: 1, l: 2} | seen again |
| o | {h: 1, e: 1, l: 2, o: 1} | new |
Cost model
Annotate
In Count letters with defaultdict(int), before reading the notes: mark where the time actually goes. Which line dominates?
Anomaly
Predict first
A student writes this, and it looks reasonable:
You read a key that a normal dict has never seen.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Ordinary dicts have no default, so scores['ben'] raises KeyError: 'ben' and the program stops.
A defaultdict(int) fills in 0 instead of crashing.
Why: Ordinary dicts have no default, so scores['ben'] raises KeyError: 'ben' and the program stops.
Trap
You read a key that a normal dict has never seen.
scores = {"ana": 90}
print(scores["ben"])A missing key on a plain dict crashes
Why: Ordinary dicts have no default, so scores['ben'] raises KeyError: 'ben' and the program stops.
| you write | result |
|---|---|
| scores['ben'] | KeyError: 'ben' |
A defaultdict(int) fills in 0 instead of crashing.
from collections import defaultdict
d = defaultdict(int)
print(d["new"])The missing key defaults to 0
Why: Verified by execution: prints 0, and the key 'new' is now stored as 0. Pick the container that matches how you read it.
| you write | prints |
|---|---|
| d['new'] | 0 |
Error analysis
Annotate
Walk the callouts on Trap: a plain dict raises KeyError. Each one is a place this is easy to get subtly wrong.
Section
Part 7
Concept
os talks to the operating system - the current folder, environment, and file operations. sys describes the running Python itself and its command-line arguments.
You will meet these in depth later; for now, know they exist and where to look.
Analogy
Discussion prompt
Explain os and sys reach outside your code by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
os talks to the operating system - the current folder, environment, and file operations. sys describes the running Python itself and its command-line arguments.
Pattern
Predict first
The table runs: sys.version_info.major | 3 · sys.version_info.minor | 12
In Peek at the environment, given the rows so far: what is the next one — the row where expression is sys.platform?
Correct: sys.platform | win32
| expression | value (this run) |
|---|---|
| sys.version_info.major | 3 |
| sys.version_info.minor | 12 |
| sys.platform | win32 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Its .major and .minor fields are the version numbers - useful when code needs a certain Python.
Worked example
import sys
print(sys.version_info.major, sys.version_info.minor)
print(sys.platform)sys.version_info reports the Python version
Why: Its .major and .minor fields are the version numbers - useful when code needs a certain Python.
sys.platform names the OS family
Why: Verified by execution on this machine: 3 12, then win32 (Windows). On other systems it reads linux or darwin.
| expression | value (this run) |
|---|---|
| sys.version_info.major | 3 |
| sys.version_info.minor | 12 |
| sys.platform | win32 |
Trade off
Comparison matrix
From Peek at the environment: every row here is a choice with a cost. Fill the value (this run) column, then say which row you would actually pick and what you give up for it.
| expression | value (this run) |
|---|---|
| sys.version_info.major | 3 |
| sys.version_info.minor | 12 |
| sys.platform | win32 |
Concept
Before you hand-write square roots, date math, counting loops, or path parsing, check the library. The built-in version is tested, faster, and handles edge cases you would miss.
Writing your own is a great way to learn, but for real programs, reach for the tool that already works.
Counterexample
Discussion prompt
Before you hand-write square roots, date math, counting loops, or path parsing, check the library. The built-in version is tested, faster, and handles edge cases you would miss.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Writing your own is a great way to learn, but for real programs, reach for the tool that already works.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Guessing a square root by hand - slow, and only right for perfect squares you happen to know.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: This prints 144 - it confirms 12 squared, but it never actually finds the root of a number you do not already know.
Ask the library for the root directly.
Why: This prints 144 - it confirms 12 squared, but it never actually finds the root of a number you do not already know.
Trap
Guessing a square root by hand - slow, and only right for perfect squares you happen to know.
guess = 12
print(guess * guess)You are checking, not computing
Why: This prints 144 - it confirms 12 squared, but it never actually finds the root of a number you do not already know.
| you write | prints |
|---|---|
| guess * guess | 144 |
Ask the library for the root directly.
import math
print(math.sqrt(144))math.sqrt computes it for any number
Why: Verified by execution: 12.0 - and it works for 2, 7, or 1000 just the same, no guessing.
| you write | prints |
|---|---|
| math.sqrt(144) | 12.0 |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
pip - it is already on your machine.; A module sits idle until you import it. After import math, you reach its tools through the module name: math.sqrt(...).; import math brings in the whole module; you then write math.sqrt. from datetime import date pulls one name in so you write date directly.Section
Part 8
Pattern
1. import the module at the top of the file
Why: import math, import random, from datetime import date - do it once, up top.
2. Reach tools through the module name
Why: math.sqrt(...), random.randint(...) - the dot says 'this tool lives in that module'.
3. Check the docs for what it already does
Why: docs.python.org lists every function; the answer to 'is there a built-in for this?' is usually yes.
4. Seed anything random you want to reproduce
Why: random.seed(n) before the draws makes the run repeatable for tests and demos.
Pattern
Roots, rounding, pi -> math
Why: math.sqrt, math.floor, math.ceil, math.pi cover the common arithmetic.
Dice, picks, shuffles -> random (with a seed)
Why: randint, choice, shuffle - seed first when you need the same result twice.
Days and calendars -> datetime
Why: date, timedelta, strftime handle date math and formatting so you never count days by hand.
Files, paths, tallies -> pathlib and collections
Why: Path for locations, Counter and defaultdict for counting and grouping.
Real world
Discussion prompt
Outside this lesson: where does Session 22 - Standard Library Tour actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Reach for the library first is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 22 of the Python Fundamentals series: the 'batteries included' tour. A guided, runnable walk through the modules you reach for daily - math (sqrt, floor, ceil, pi), random (randint, choice, shuffle, and seed for reproducibility), datetime (date, timedelta, strftime), pathlib.Path (the / operator, name/suffix/stem, exists), and collections (Counter, defaultdict) - plus a look at os and sys.
Check
Round it the floor way.
import math
print(math.floor(4.9))| call | returns |
|---|---|
| math.floor(4.9) | ? |
Check your understanding
What does this print?
Answer: A
Why: floor always rounds down to the nearest whole number, so math.floor(4.9) is 4 (an int). Verified by execution.
Check
The seed is set twice to the same value.
import random
random.seed(1)
print(random.randint(1, 100))
random.seed(1)
print(random.randint(1, 100))| step | prints |
|---|---|
| seed(1); randint | 18 |
| seed(1); randint | ? |
Check your understanding
What is the second line of output?
Answer: A
Why: Re-seeding with the same value 1 rewinds the sequence, so the next randint is identical: 18 both times. Verified by execution.
Comparison
Comparison matrix
From Check: seed reproducibility: refill the prints column from what you know. The rest of the table is as it appeared.
| step | prints |
|---|---|
| seed(1); randint | 18 |
| seed(1); randint | ? |
Check
Count the s's.
from collections import Counter
c = Counter("mississippi")
print(c["s"])| word | letter | count |
|---|---|---|
| mississippi | s | ? |
Check your understanding
What does this print?
Answer: A
Why: Counter tallies each character; 'mississippi' has four s's, so c['s'] is 4. Verified by execution.
Check
Step back one day across a month boundary.
from datetime import date, timedelta
d = date(2026, 3, 1)
print(d - timedelta(days=1))| expression | value |
|---|---|
| d - timedelta(days=1) | ? |
Check your understanding
What does this print?
Answer: A
Why: One day before March 1, 2026 is the last day of February. 2026 is not a leap year, so February ends on the 28th. Verified by execution.
Check
What is the extension?
from pathlib import Path
print(Path("archive.tar.gz").suffix)| path | suffix |
|---|---|
| archive.tar.gz | ? |
Check your understanding
What does .suffix return here?
Answer: A
Why: .suffix is only the last extension, so it is '.gz'. To get both, .suffixes returns ['.tar', '.gz']. Verified by execution.
Check
Read a key that was never set.
from collections import defaultdict
d = defaultdict(int)
print(d["new"])| factory | d['new'] |
|---|---|
| int | ? |
Check your understanding
What does this print?
Answer: A
Why: defaultdict(int) creates a missing key using int(), which is 0, so d['new'] is 0 and no error is raised. Verified by execution.
Check
No import line - what happens?
print(sqrt(16))| has import? | result |
|---|---|
| no | ? |
Check your understanding
What does this produce?
Answer: A
Why: sqrt lives in the math module. Without import math and the math. prefix, the bare name is undefined, raising NameError: name 'sqrt' is not defined. Verified by execution.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Batteries Included · math: Numbers Done Right · random: Controlled Chance · datetime: Dates & Days · pathlib: Paths as Objects · collections: Better Containers. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
The standard library ships with Python. import a module, then reach its tools through the module name - and reach for it before writing your own.
| Reach for | When you need |
|---|---|
| math.sqrt / floor / ceil / pi | roots, rounding, and the constant pi |
| random + seed(n) | dice, picks, shuffles - reproducibly |
| date / timedelta / strftime | calendar days, gaps, and formatting |
| Path | path pieces, the / join, and .exists() |
| Counter / defaultdict | fast tallying and auto-defaulting keys |
The habit that matters most: assume the job is already solved, and go look. Next session we package your own reusable code into a module others can import.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.