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:
- Four ways to drop a fraction:
math.floor(down, toward minus infinity),math.ceil(up),int()/math.trunc(toward zero), andround()(nearest, with exact halves going to the even neighbor, soround(2.5)is 2 andround(3.5)is 4). For negative numbers they disagree, as one exercise shows. Pagination ("how many pages for 41 items at 10 per page?") wantsmath.ceil(41 / 10). math.log(1000, 10)is2.9999999999999996, not 3: it computeslog(1000) / log(10)in binary floating point. Usemath.log10for base 10 andmath.iscloserather than==when comparing floats.mathworks on real numbers:math.sqrt(-1)raisesValueError: math domain error. Thecmathmodule handles complex numbers. For exact decimal money arithmetic usedecimal.Decimal; for exact ratiosfractions.Fraction.
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
randomfor passwords, tokens or anything security-related. Its output can be predicted after observing enough values. Usesecrets.token_hex(16)orsecrets.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
dateis a calendar day,datetimea day plus a time,timedeltaa duration. Subtracting two dates gives a timedelta; adding a timedelta to a date gives a date. Python knows 2024 was a leap year.strftime("string format time") turns a date into text using codes:%Yyear,%mmonth number,%dday,%H:%M24-hour time,%Aweekday name,%Bmonth name. Names follow the locale; in the default C locale they are English.strptimeandfromisoformatgo the other way, parsing text.- ISO 8601 (
2026-11-01T14:30:00) is the format to use in files and logs: unambiguous, and it sorts correctly as plain text. datetime.now()anddate.today()read the system clock. They are fine in programs, but code whose output depends on them is hard to test; pass the date in as an argument where you can. (This is why every exercise here uses fixed dates.)
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
- Testing float results with
==. Usemath.isclose, or round for display. - Seeding inside a loop.
random.seed(1)before every roll makes every roll identical. Seed once. - Expecting
random.shuffleto return the list. It shuffles in place and returnsNone:cards = random.shuffle(cards)setscardstoNone. - Hand-rolled date math. "Add 30 days by bumping the month" breaks constantly. Use
timedelta, and for "same day next month" logic think carefully about January 31. - Comparing naive and aware datetimes:
TypeError: can't compare offset-naive and offset-aware datetimes. Pick one kind (aware UTC) throughout. os.environ["X"]for optional settings raisesKeyErrorwhen unset; use.getwith a default.
Recap
mathcovers floor/ceil/trunc/round (they differ for negatives and halves), roots, logs, gcd, comb, andisclosefor float comparison.randomis a seeded, deterministic PRNG: seed for reproducibility, userandom.Random(seed)for a private generator, andsecretsfor security.datetimegivesdate,datetimeandtimedeltawith correct calendar arithmetic,strftime/fromisoformatfor text, and time zones viazoneinfo.os.environholds environment variables;os.pathandpathlibmanipulate paths,pathlibbeing the modern choice.
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.