Python ships with a large standard library, and knowing what is in it saves you from writing code that already exists or installing a package you do not need. This unit is a guided tour of the modules you will reach for most often: dates and times, random numbers and secrets, maths and statistics, the operating system and the interpreter, running other programs, copying files, command line options, logging, temporary files and a built in SQL database.
You will not learn every function. The goal is to know which module solves which problem, what its safe default is, and where the traps are. The Python documentation has the full reference for each module.
You will be able to:
- Create, format, parse and compare dates and times, and convert between time zones with
zoneinfo. - Choose between
randomandsecrets, and between the clocks intime. - Use
math,statistics,decimalandfractionsfor exact and statistical calculations. - Read environment variables and command line arguments, and run another program safely with
subprocess. - Build a command line interface with
argparseand report what a program is doing withlogging. - Work with temporary files and an in memory SQLite database.
Dates and times with datetime
The datetime module has four main types: date (a calendar day), time (a time of day), datetime (both together) and timedelta (a duration). They support arithmetic and comparison, and they convert to and from text.
strftime() ("string format time") turns a date into text, and strptime() ("string parse time") does the reverse with the same codes. Since 3.14, the date and time classes have their own strptime() as well. The common codes:
| Code | Meaning | Example |
|---|---|---|
%Y / %y | year, 4 or 2 digits | 2026 / 26 |
%m / %d | month / day, zero padded | 11 / 05 |
%H / %I | hour, 24 hour / 12 hour clock | 21 / 09 |
%M / %S | minute / second | 05 / 00 |
%p | AM or PM | PM |
%A / %a | weekday name, full / short | Wednesday / Wed |
%B / %b | month name, full / short | November / Nov |
%j | day of the year | 315 |
%z / %Z | UTC offset / time zone name | +0100 / CET |
Names such as %A follow the current locale, which is the English "C" locale unless your program changes it. For data exchange, prefer ISO 8601 text (2026-11-11T14:30:00) with isoformat() and fromisoformat(). Since 3.11, fromisoformat() accepts most ISO 8601 forms, including a trailing Z for UTC.
Time zones: aware and naive datetimes
A naive datetime has no time zone (tzinfo is None): it could be 09:00 anywhere. An aware datetime carries a time zone, so it names one exact instant. Python refuses to order the two kinds against each other.
datetime.UTC (since 3.11) is a short name for timezone.utc. To get the current time, use datetime.now(UTC). The older datetime.utcnow() returns a naive value that silently means UTC; it has been deprecated since 3.12 and emits a DeprecationWarning.
For real time zones, with their daylight saving rules, use zoneinfo (standard since 3.9) and the IANA names such as Europe/London:
The clocks in London went forward between the two instants. Subtracting two datetimes that share the same tzinfo gives the wall clock difference (2 hours); converting both to UTC first gives the time that really passed (1 hour).
Pitfall. Windows has no system time zone database, so
ZoneInfo("Europe/London")raisesZoneInfoNotFoundErrorthere until you install thetzdatapackage:python -m pip install tzdata. Store and compute in UTC, and convert to local time only for display.
Measuring time: the time module
The time module offers several clocks, each for a different job:
| Function | Use it for | Notes |
|---|---|---|
time.time() | timestamps: seconds since 1 January 1970 UTC | can jump if the system clock is adjusted |
time.monotonic() | timeouts and deadlines | never goes backwards |
time.perf_counter() | measuring how long code takes | highest available resolution |
time.sleep(seconds) | pausing | accepts floats such as 0.25 |
Your output will differ from the one shown, because it depends on when and where you run it:
For careful microbenchmarks, the timeit module repeats a snippet many times and reports the best run.
Calendars: calendar
calendar answers calendar questions and prints text calendars. Weeks start on Monday by default.
leapdays(y1, y2) counts leap years from y1 up to but not including y2. monthrange() returns the weekday of the first day (0 is Monday) and the number of days in the month. For a calendar that starts on Sunday, create calendar.TextCalendar(firstweekday=calendar.SUNDAY) and call its formatmonth() method. day_name and month_name follow the current locale; treat them as read only.
Random numbers: random and secrets
random produces pseudorandom numbers: a deterministic algorithm that looks random. That is ideal for simulations, games and tests, because a seed makes the results repeatable. Create your own generator with random.Random(seed) rather than seeding the shared module level one.
randint(a, b) includes both ends. choices() picks with replacement (repeats allowed, optional weights); sample() picks without replacement. shuffle() reorders the list in place and returns None. Without a seed, the generator is seeded from the operating system's randomness source.
Because the algorithm is deterministic, anyone who sees enough outputs can predict the next ones. For passwords, tokens and anything else an attacker must not guess, use secrets, which draws from the operating system's cryptographically secure source:
Your output will differ every time. To compare a token someone sends you with the stored one, use secrets.compare_digest(a, b), which takes the same time whether the strings differ early or late.
Pitfall. Never use
randomfor passwords, session tokens or one time codes, even though the code looks identical.random.choiceandsecrets.choicehave the same signature; only one of them is safe.
Numbers: math, statistics, decimal and fractions
math holds the classic functions, and statistics the common descriptive statistics. Values, names and types explains why 0.1 + 0.2 is not exactly 0.3; decimal and fractions give you exact arithmetic when you need it.
What to notice:
pstdevis the population standard deviation andstdevthe sample one (dividing by n − 1).round(2.675, 2)gives 2.67 because the float closest to 2.675 is slightly below it.Decimalwith an explicit rounding rule gives the answer an accountant expects.- Build a
Decimalfrom a string.Decimal(0.1)faithfully copies the binary float, error included.
The operating system: os and os.environ
os is the low level interface to the operating system. For paths and files, prefer pathlib (Files and data formats); os remains the place for environment variables, process information and a few file operations.
Environment variables are name and value strings that a process inherits from whoever started it. They are the standard way to pass configuration and secrets such as API tokens to a program without writing them in the source code.
os.environ["NAME"] raises KeyError for a missing variable, which is right for a setting your program cannot run without. os.environ.get() returns a default instead. os.name is "nt" on Windows and "posix" on macOS and Linux. Other useful functions: os.getcwd(), os.listdir(), os.walk() and os.cpu_count().
The interpreter: sys
sys describes the running interpreter. sys.argv is the list of command line arguments, with the script name first; sys.path is the list of folders searched for imports (Modules and packages); sys.version_info lets you check the Python version.
A script reports success or failure through its exit status: 0 means success, anything else an error. A common pattern is a main() function that returns the status:
In a real script the last lines would be if __name__ == "__main__": sys.exit(main(sys.argv)). sys.exit(status) ends the program with that status; the usage message went to sys.stderr, the stream for errors, so it does not mix with normal output.
Running other programs: subprocess
subprocess.run() starts a program, waits for it, and returns a CompletedProcess. Pass the command as a list: the program first, then one item per argument. The examples run Python itself (sys.executable) so they work on every system.
capture_output=Truecollects the child's output instead of letting it print, andtext=Truedecodes it tostr.check=TrueraisesCalledProcessErrorwhen the exit status is not 0. Without it, failures pass silently.timeout=kills a child that hangs and raisesTimeoutExpired.- With a list, the user's text stays one argument: the
;is an ordinary character, not a command separator.
Pitfall.
shell=True,os.system()andos.popen()hand a command string to the shell, which interprets;,&&,|and quotes. If any part of that string comes from a user or a file, an attacker can append their own command. Usesubprocess.run()with a list and leaveshellat its default ofFalse.
Copying and removing files: shutil
shutil ("shell utilities") does the high level file operations that pathlib lacks or that work on whole trees.
shutil.which() finds a program on the PATH (or returns None), and shutil.make_archive() creates a ZIP or tar archive of a folder.
Pitfall.
shutil.rmtree()deletes permanently: there is no recycle bin and no confirmation. Check the path before you call it, never build it from unchecked user input, and test destructive scripts on a copy, or behind a "dry run" option that only prints what it would do.
Command line interfaces: argparse
argparse turns sys.argv into named, typed, validated options, and writes the --help text and the error messages for you. You describe the arguments, then call parse_args(). A real script calls it with no arguments so that it reads sys.argv; the example passes a list instead, which is also how you test a parser.
- Names without dashes are positional and required;
nargs="+"means one or more. - Options start with
-or--;type=intconverts the text,choicesrestricts it, andaction="store_true"makes a flag that isFalseunless given. - Dashes in option names become underscores in attributes (
--ignore-case→args.ignore_case);destpicks another name. - By default a bad argument prints the usage line and an error to
stderrand exits with status 2.-h/--helpis added automatically.
Logging
print() is fine for a program's real output, but diagnostic messages belong in logging: each message has a level, you can switch levels on and off without editing code, and the output can go to the console, a file or a log service.
| Level | Value | Use it for |
|---|---|---|
DEBUG | 10 | details for diagnosing problems |
INFO | 20 | confirmation that things work as expected |
WARNING | 30 | something unexpected, but the program carries on (the default threshold) |
ERROR | 40 | a failure of one operation |
CRITICAL | 50 | the program cannot continue |
Call basicConfig() once, at the start of the program, and give each module its own logger with logging.getLogger(__name__), so every message shows where it came from. Pass values as arguments (log.info("loaded %d items", n)) rather than formatting the string yourself: the text is only built if the message is actually emitted.
Inside an except block, log.exception() logs at ERROR level and appends the full traceback. Your output will differ in the file name and line numbers:
Temporary files: tempfile
tempfile creates files and folders with unique names in the system's temporary directory, and removes them for you.
TemporaryDirectory deletes the folder and its contents when the with block ends. With delete_on_close=False (since 3.12), closing a named temporary file does not delete it, so you can close it and let another open() call, or another program, read it by name; the file is removed when the with block ends. That is the portable way to hand a temporary file to something else, Windows included.
A built in database: sqlite3
SQLite is a complete SQL database stored in a single file, and sqlite3 is in the standard library. ":memory:" creates a throwaway database in memory, which is handy for tests and examples. SQL, the query language, is a subject of its own that this course does not teach. The example uses only its most basic statements.
Pitfall. Never build SQL by pasting values into the string, as in
f"... WHERE title = '{title}'". A title such asx' OR '1'='1changes the meaning of the query (SQL injection). The?placeholders send values separately from the SQL, so they can never be read as SQL. Note too thatwith con:manages the transaction but does not close the connection.
Check yourself
- What is the difference between a naive and an aware datetime, and why is
datetime.utcnow()deprecated? - You subtract two London datetimes on either side of a clock change. Why might the answer be off by an hour, and how do you get the real elapsed time?
- Which clock should you use to time a piece of code, and which to implement a timeout?
- Why is
random.choicewrong for generating a password, whensecrets.choicehas the same signature? - Why should you write
Decimal("0.1")rather thanDecimal(0.1)? - Why pass a list to
subprocess.run()instead of a string withshell=True? What doescheck=Trueadd? - Where does
loggingsend its messages by default, and which levels are shown? - Why use
?placeholders insqlite3queries?
Suggested answers
- A naive datetime has no
tzinfo, so it does not name an exact instant; an aware one does.utcnow()returns a naive value that silently means UTC, which is easy to mix up with local time;datetime.now(UTC)returns an aware value instead. - When both share the same
tzinfo, subtraction uses wall clock times, so a clock change adds or removes an hour. Convert both to UTC before subtracting. time.perf_counter()for timing code;time.monotonic()for timeouts, because it never goes backwards.randomuses a deterministic generator whose future output can be predicted from past output.secretsdraws from the operating system's cryptographically secure source.0.1is already a binary float with a tiny error, andDecimal(0.1)copies that error exactly. The string gives the exact decimal value.- With a list, each item is passed as one argument and no shell interprets the text, so user input cannot inject extra commands.
check=TrueraisesCalledProcessErrorif the program exits with a nonzero status. - To
sys.stderr; the default threshold is WARNING, so WARNING, ERROR and CRITICAL are shown and DEBUG and INFO are not. - Placeholders keep values separate from the SQL text, which prevents SQL injection and handles quoting and types for you.
Exercises
These exercises are optional practice. Pick the ones that help you.
-
Days ahead. Read a start date as
YYYY MM DDand a number of days N from two lines of input. Print N lines, one per day from the start date, as2026-11-13 Fri, adding(weekend)for Saturdays and Sundays. A negative N is an error. For2026 11 13and4the second line is2026-11-14 Sat (weekend). -
Day of the week. Read a date as
YYYY MM DDand print the name of its weekday.2026 11 11givesWednesday. -
Random date. Write
random_date(start, end, rng)that returns a uniformly random date between two dates, both included, using therandom.Randominstance passed in. RaiseValueErrorifendis beforestart. Show that the same seed gives the same date, and that equal start and end dates give that date back. The date you get for a seed depends on how you pick it, so it may differ from the solution's. -
Repeated timestamp. Write
print_timestamps(n, delay)that prints the current timentimes,delayseconds apart, with millisecond precision. Make it testable without real waiting by accepting the clock and the sleep function as keyword arguments. -
Directory tree. Write
tree_lines(root)that returns the lines of an indented tree: folders end with/, each level adds four spaces, and in each folder subfolders come first, then files, each group sorted case insensitively. Build a temporary tree to test it. For a folderRootDirthat holdsfile.txt,file.py,NextDir/new_file.txt,NewDir/new_file.pyandNewDir/deeper/data.csv, the lines are: -
Last modified. Write
last_modified(path)that returns a file's modification time in ISO 8601 format in UTC, to the second, such as2026-02-22T09:55:23+00:00. Test it on a file whose time you set withos.utime(). -
Python folders. Write
dirs_with_python(root)that returns the sorted relative paths (with/separators) of every folder belowroot, includingrootitself as".", that directly contains at least one.pyfile. For a tree withsetup.py,app/main.py,app/utils/helpers.py,docs/index.md,scripts/run.pyand an emptydatafolder, the answer is['.', 'app', 'app/utils', 'scripts'].
Solutions
1. Days ahead.
A date works inside an f-string format spec, so {day:%a} is the same as day.strftime("%a").
2. Day of the week.
3. Random date.
randint includes both ends, so the end date can be chosen too. Passing the generator in, rather than calling module level random functions, is what makes the function reproducible and testable.
4. Repeated timestamp.
The fake clock makes the output predictable and the test instant. The last run sleeps for real but reads the frozen clock, so it checks the waiting without printing changing times. Called as print_timestamps(3, 1), the function uses the real clock.
5. Directory tree.
The sort key (p.is_file(), name) puts folders first because False sorts before True.
6. Last modified.
Without tz=UTC, fromtimestamp() returns a naive local time, which depends on the machine that runs the code.
7. Python folders.
A set removes the duplicates when one folder holds several .py files, and rglob searches every level for you.