Modules, packages and the standard library · about 50 min

Standard library tour II: math, random, datetime and os

Why this matters

Numbers beyond + - * /, randomness for simulations and tests, dates and times, and information about the system your program runs on come up in almost every real project. Each has traps for the unwary: rounding toward the wrong direction, "random" numbers that are predictable (or that you cannot reproduce when you need to), date arithmetic done by hand that breaks on leap years, and hard-coded paths. The standard library already handles all of these correctly. This lesson tours the four modules you will reach for most.

math

math provides functions on floats and integers, implemented in C on top of the platform's math library:

import math

print(math.sqrt(2), math.pi, math.e)
print(math.floor(2.7), math.ceil(2.1), math.trunc(-2.7))
print(math.hypot(3, 4), math.dist((0, 0), (6, 8)))
print(math.gcd(12, 18), math.lcm(4, 6), math.factorial(10), math.comb(5, 2))
print(math.log(1000, 10), math.log2(1024), math.isclose(0.1 + 0.2, 0.3))
print(math.inf > 10**100, math.isnan(float("nan")))
1.4142135623730951 3.141592653589793 2.718281828459045
2 3 -2
5.0 10.0
6 12 3628800 10
2.9999999999999996 10.0 True
True True

Points worth knowing:

random

Pseudo-random numbers

import random

random.seed(42)
print(random.random())                    # float in [0.0, 1.0)
0.6394267984578837

Run that on any machine with CPython and you get the same number. random does not produce true randomness; it is a pseudo-random number generator (PRNG), specifically the Mersenne Twister: a deterministic algorithm with a large internal state (about 2.5 KB). Each call advances the state and derives a number from it. The seed sets the starting state, so a seed fixes the entire future sequence. Without an explicit seed, Python seeds from the operating system's entropy source (os.urandom) at import, so each run differs.

The main functions:

random.randint(1, 6)          # integer, both ends included
random.randrange(0, 10, 2)    # like range(): 0, 2, 4, 6 or 8
random.uniform(1.5, 2.5)      # float in the range
random.choice(["a", "b"])     # one element
random.sample(range(100), 5)  # 5 distinct elements
random.shuffle(my_list)       # shuffles in place, returns None

Reproducibility and private generators

Seeding is how you make "random" programs testable: a simulation with seed=7 gives the same result every run, so you can compare outputs and debug. The module-level functions share one hidden global generator, so any other code that calls random changes the sequence you see. For reproducible results, create your own generator object:

rng = random.Random(3)
cards = list(range(1, 11))
rng.shuffle(cards)
print(cards)
[2, 6, 7, 1, 10, 5, 8, 3, 9, 4]

random.Random(seed) has all the same methods, with its own state untouched by anyone else.

Warning: never use random for passwords, tokens or anything security-related. Its output can be predicted after observing enough values. Use secrets.token_hex(16) or secrets.choice(...), which draw from the operating system's cryptographic source.

datetime

The datetime module represents calendar dates and times as objects, so arithmetic respects month lengths and leap years.

from datetime import date, datetime, timedelta

launch = date(2026, 11, 1)
new_year = date.fromisoformat("2027-01-01")
gap = new_year - launch
print(gap, gap.days, type(gap).__name__)
print(new_year.strftime("%A %d %B %Y"))
print(date(2024, 2, 28) + timedelta(days=1))

meeting = datetime(2026, 11, 1, 14, 30)
print(meeting.isoformat(), meeting + timedelta(hours=12))
print(meeting.strftime("%Y-%m-%d %H:%M"))
61 days, 0:00:00 61 timedelta
Friday 01 January 2027
2024-02-29
2026-11-01T14:30:00 2026-11-02 02:30:00
2026-11-01 14:30

Time zones, briefly

A datetime without time zone information is naive: it does not say where 14:30 is. For anything that crosses time zones or daylight-saving changes, use aware datetimes: datetime.now(timezone.utc) for UTC, and zoneinfo.ZoneInfo("America/New_York") for named zones. A good rule: store and compute in UTC, convert to local time only for display.

The separate time module is lower level: time.time() returns seconds since the Unix epoch (1970-01-01 UTC) as a float, time.sleep(s) pauses, and time.perf_counter() is the right clock for measuring how long code takes.

os: talking to the operating system

os wraps operating-system services. The pieces you will use most:

import os

print(os.getcwd())                       # current working directory
print(os.environ.get("HOME"))            # environment variable, or None
print(os.environ.get("EDITOR", "nano"))  # with a default
print(os.listdir("."))                   # names in a directory
print(os.getpid())                       # this process's id

Environment variables are key-value strings that every process inherits from its parent: HOME, PATH, LANG, and whatever you set (export DEBUG=1, or DEBUG=1 python3 app.py for one command). os.environ is a dictionary-like view of them. Programs commonly take configuration this way, especially in containers and services, because it needs no files and no argument parsing. Values are always strings: int(os.environ.get("PORT", "8080")).

os.path versus pathlib

os.path is the older, string-based way to manipulate paths:

import os.path
p = os.path.join("data", "2026", "report.csv")
print(p, os.path.basename(p), os.path.splitext(p)[1], os.path.exists(p))
data/2026/report.csv report.csv .csv False

pathlib, which you met in the files module, does the same with objects: Path("data") / "2026" / "report.csv", .name, .suffix, .exists(). Prefer pathlib in new code; you will still read plenty of os.path in existing code. shutil complements both with high-level operations: shutil.copy, shutil.move, shutil.rmtree (delete a directory tree, carefully) and shutil.which("git").

Common mistakes

Recap

Going further: in the REPL, run import this, then browse the module index of the documentation (docs.python.org/3/py-modindex.html) and pick three modules you have never heard of. statistics, textwrap and difflib are good starts.

Exercises

Exercise 1 · quiz

Four ways to drop the fraction

What does this print?

import math
x = -2.5
print(math.floor(x), math.ceil(x), int(x), round(x))
Answer
  • -3 -2 -2 -2

math.floor rounds toward minus infinity (-3), math.ceil toward plus infinity (-2), int() truncates toward zero (-2), and round() rounds to the nearest integer with ties going to the even neighbor ("banker's rounding"), so -2.5 becomes -2 (and 3.5 would become 4). They only all agree for positive numbers without ties, which is why choosing the right one matters for things like money and pagination.

Exercise 2 · quiz

What a seed guarantees

A simulation calls random.seed(1234) at the start and then uses random.randint and random.shuffle. Which statement is true?

Answer
  • Every run of the program with the same Python produces exactly the same sequence of numbers

random is a pseudo-random generator (the Mersenne Twister): a deterministic algorithm whose entire output is fixed by its starting state. The same seed gives the same sequence for every function built on it, which is perfect for reproducible simulations and tests. That same predictability makes it unsuitable for security: use the secrets module for tokens and passwords.

Exercise 3 · write code

Reproducible dice

Write a dice simulator that takes two command-line arguments: a seed and a number of rolls. Create a private generator with random.Random(seed) (using the seed converted to int), roll a six-sided die that many times with its randint(1, 6) method, and print:

rolls: <the rolls separated by single spaces>
total: <sum>

For python3 main.py 1 5 the output is:

rolls: 2 5 1 3 1
total: 12

Make each roll with one randint(1, 6) call, in order, so the sequence matches. Arguments are in sys.argv[1] and sys.argv[2].

File: main.py

Starting point

import random
import sys

Example with arguments 1 5

expected output

rolls: 2 5 1 3 1
total: 12

Example with arguments 42 3

expected output

rolls: 6 1 1
total: 8
Hint 1

rng = random.Random(int(sys.argv[1])) makes a generator with its own state.

Hint 2

A list comprehension: [rng.randint(1, 6) for _ in range(count)].

Hint 3

" ".join(...) needs strings: convert each roll with str.

Reference solution
import random
import sys

seed = int(sys.argv[1])
count = int(sys.argv[2])
rng = random.Random(seed)
rolls = [rng.randint(1, 6) for _ in range(count)]
print("rolls:", " ".join(str(r) for r in rolls))
print("total:", sum(rolls))

Exercise 4 · write code

Date arithmetic

Read two dates in ISO format (YYYY-MM-DD), one per line. The second is never earlier than the first. Print three lines:

days apart: <whole days from the first to the second>
weekday: <full English weekday name of the second date>
plus 100 days: <the second date plus 100 days, in ISO format>

For 2026-11-01 and 2027-01-01:

days apart: 61
weekday: Friday
plus 100 days: 2027-04-11

Use datetime.date.fromisoformat, subtraction (which gives a timedelta), strftime("%A") and timedelta(days=100).

File: main.py

Starting point

from datetime import date, timedelta

first = input()
second = input()

Example

input

2026-11-01
2027-01-01

expected output

days apart: 61
weekday: Friday
plus 100 days: 2027-04-11

Example

input

2000-02-01
2000-03-01

expected output

days apart: 29
weekday: Wednesday
plus 100 days: 2000-06-09
Hint 1

date.fromisoformat("2026-11-01") parses the text into a date object.

Hint 2

second - first is a timedelta; its .days attribute is the whole number of days.

Hint 3

Printing a date shows ISO format; .isoformat() makes that explicit.

Reference solution
from datetime import date, timedelta

first = date.fromisoformat(input().strip())
second = date.fromisoformat(input().strip())
print("days apart:", (second - first).days)
print("weekday:", second.strftime("%A"))
print("plus 100 days:", (second + timedelta(days=100)).isoformat())

Exercise 5 · write code

Configuration from the environment

Programs often read settings from environment variables. Write a program that prints a greeting built from two of them:

Output format, exactly: <GREETING>, <NAME>!

With neither variable set, it prints Hello, stranger!. Run as GREETING=Howdy NAME=Ada python3 main.py, it prints Howdy, Ada!. The checker runs your program with several combinations. Use os.environ.get.

File: main.py

Starting point

import os

print("Hello, stranger!")
Hint 1

os.environ behaves like a dictionary of strings.

Hint 2

os.environ.get("NAME", "stranger") returns the default when the variable is missing.

Reference solution
import os

greeting = os.environ.get("GREETING", "Hello")
name = os.environ.get("NAME", "stranger")
print(f"{greeting}, {name}!")