Standard library tour: itertools, functools, pathlib, and friends
The dozen modules that turn a short script into idiomatic Python — with the anti-patterns each replaces.
Why this matters
Python's standard library is famously "batteries included." Most one-off scripts can be written without pip install. Senior developers reach for collections, itertools, functools, pathlib, datetime, subprocess, hashlib, secrets, os, tempfile, argparse, and a dozen others by reflex. This tour gives you the lay of the land and the patterns you'll use weekly.
Learning objectives
- Know the 20 modules you'll actually use.
- Use
collectionsanditertoolsidiomatically. - Manipulate dates and times correctly (timezones!).
- Spawn subprocesses safely.
- Use OS, env, paths, temp files, hashing.
1. The "must-know" 20
| Module | What |
|---|---|
os, os.path | OS interaction; mostly replaced by pathlib for paths |
sys | Interpreter introspection, argv, stdin/out/err |
pathlib | Object-oriented paths |
io | In-memory streams; file abstraction |
tempfile | Temp files / dirs that clean up |
shutil | High-level file ops (copy, move, rmtree, archive) |
glob | File pattern matching (or Path.glob) |
re | Regex |
collections | Counter, defaultdict, OrderedDict, deque, ChainMap, namedtuple |
itertools | Iterator combinators |
functools | partial, lru_cache, cache, reduce, wraps, cached_property, singledispatch |
datetime, zoneinfo | Dates, times, timezones |
time | Sleep, monotonic clock, perf counter |
json, csv, tomllib | Common formats |
subprocess | Run external commands |
argparse (or typer, click) | CLI args |
logging | Logs |
secrets, hashlib, hmac | Crypto-grade randomness, hashing |
concurrent.futures | Thread/Process pools |
asyncio | Async runtime (Phase 4.4) |
This lesson hits the ones not already covered. pathlib, json, csv, logging, re, asyncio, concurrent.futures get their own lessons.
2. collections — every type you wished was built-in
from collections import Counter, defaultdict, deque, OrderedDict, ChainMap, namedtuple(Recap from Lesson 1.2. Key idioms:)
# Frequency
Counter("abracadabra").most_common(3) # [('a', 5), ('b', 2), ('r', 2)]
# Grouping
buckets = defaultdict(list)
for w in words: buckets[len(w)].append(w)
# Fixed-size queue (rolling window)
window = deque(maxlen=10)
for x in stream: window.append(x); use(list(window))
# Layered config
cfg = ChainMap(env, file_cfg, defaults)namedtuple is largely superseded by dataclass and NamedTuple. Keep it in mind for legacy code.
3. itertools — combinators
from itertools import chain, accumulate, count, cycle, islice, groupby, batched, pairwise
list(chain([1,2], [3,4])) # [1,2,3,4]
list(accumulate([1,2,3,4])) # [1,3,6,10] (cumulative)
list(islice(count(10, 2), 5)) # [10,12,14,16,18]
list(pairwise([1,2,3,4])) # [(1,2),(2,3),(3,4)]
list(batched(range(10), 3)) # [(0,1,2),(3,4,5),(6,7,8),(9,)] (3.12+)
for k, group in groupby(sorted(items, key=key), key=key):
process(k, list(group))Memorize these. They eliminate boilerplate loops constantly.
4. functools — function utilities
from functools import partial, reduce, lru_cache, cache, wraps, cached_property, singledispatch, total_ordering(Covered in Lessons 1.3, 2.1, 3.1. Quick reference:)
@cache
def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)
@lru_cache(maxsize=1024)
def slow(x): ...
multiply_by_3 = partial(operator.mul, 3)
@singledispatch
def serialise(x): raise TypeError
@serialise.register
def _(x: int): return str(x)
@serialise.register
def _(x: list): return "[" + ",".join(serialise(i) for i in x) + "]"
@total_ordering
class Version: ... # define __eq__ + __lt__; get the rest5. datetime, zoneinfo, time
from datetime import datetime, date, time, timedelta, timezone
from zoneinfo import ZoneInfo
# Now — always be timezone-aware
now = datetime.now(timezone.utc)
ny = datetime.now(ZoneInfo("America/New_York"))
# Construct
d = date(2026, 6, 7)
dt = datetime(2026, 6, 7, 14, 30, tzinfo=ZoneInfo("UTC"))
# Arithmetic
dt + timedelta(days=7, hours=3)
later - earlier # timedelta
# Convert tz
ny_time = dt.astimezone(ZoneInfo("America/New_York"))
# Format / parse
dt.isoformat() # "2026-06-07T14:30:00+00:00"
datetime.fromisoformat("2026-06-07T14:30:00+00:00")
dt.strftime("%Y-%m-%d %H:%M")
datetime.strptime("2026-06-07 14:30", "%Y-%m-%d %H:%M")The "always be timezone-aware" rule
Naive datetimes (no tzinfo) are dangerous: datetime.now() returns a naive value in local time; comparing it to a UTC value silently breaks. Always pass tz=....
For human-friendly arithmetic ("end of month", "next business day"), use dateutil (uv add python-dateutil) or pendulum.
time module
import time
time.sleep(0.5) # seconds
time.monotonic() # for measuring intervals
time.perf_counter() # highest precision; for benchmarkingDon't use time.time() to measure durations — the wall clock can jump (NTP corrections).
6. os, os.environ, sys
import os, sys
os.environ.get("OPENAI_API_KEY", "")
os.environ["MY_VAR"] = "x" # set
os.getcwd() # use Path.cwd() instead
os.cpu_count() # logical cores
os.urandom(16) # 16 bytes of entropy (use `secrets` instead)
sys.argv # list of CLI args
sys.exit(0) # exit
sys.platform # 'linux', 'darwin', 'win32'
sys.version_info # (3, 12, 4, ...)
sys.stdin / sys.stdout / sys.stderr # streamsFor env loading with defaults / types, use pydantic-settings (Lesson 2.3).
7. subprocess — run external commands
import subprocess
# Simple: check exit code, capture output
r = subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True, check=True)
print(r.stdout.strip()) # commit SHA
# Don't use shell=True with user input (shell injection)
# bad: subprocess.run(f"grep {user_input} file.txt", shell=True)
# good: subprocess.run(["grep", user_input, "file.txt"])
# Stream output as it arrives
proc = subprocess.Popen(
["python", "long_script.py"],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1,
)
for line in proc.stdout:
print(line, end="")
proc.wait()
if proc.returncode:
raise RuntimeError("script failed")Always pass a list of args (not a string with shell=True) unless you genuinely need shell features.
For async, prefer asyncio.subprocess.
8. tempfile and shutil
from tempfile import TemporaryDirectory, NamedTemporaryFile, mkstemp
import shutil
with TemporaryDirectory() as tmp:
work = Path(tmp) / "work"
work.mkdir()
...
# directory auto-deleted
with NamedTemporaryFile(delete=False, suffix=".csv") as f:
f.write(b"a,b,c\n")
path = f.name
# delete manually if delete=False
shutil.copy(src, dst); shutil.copytree(src, dst)
shutil.move(src, dst)
shutil.rmtree(path)
shutil.disk_usage(".").free
shutil.which("git") # locate executable
shutil.make_archive("backup", "zip", "src/")9. hashlib, secrets, hmac
import hashlib, secrets, hmac
hashlib.sha256(b"data").hexdigest()
hashlib.blake2b(b"data", digest_size=16).hexdigest() # faster than SHA-512
# Cryptographic randomness — for tokens, passwords, IDs
secrets.token_urlsafe(32) # URL-safe random string
secrets.token_hex(16) # 32 hex chars
secrets.choice(["a", "b", "c"])
secrets.compare_digest("expected", "candidate") # constant-time
# HMAC signature
mac = hmac.new(key, msg, hashlib.sha256).hexdigest()
hmac.compare_digest(mac, received_mac)Never use random for security purposes (predictable). Use secrets.
10. argparse — quick CLI parsing
import argparse
p = argparse.ArgumentParser(description="ETL")
p.add_argument("input", type=Path, help="input file")
p.add_argument("-o", "--output", type=Path, default=Path("out.json"))
p.add_argument("--limit", type=int, default=1000)
p.add_argument("-v", "--verbose", action="store_true")
args = p.parse_args()
print(args.input, args.output, args.limit, args.verbose)argparse is fine for small scripts. For larger CLIs use typer (Phase 9 / projects) — type hints become the CLI.
11. concurrent.futures — easiest parallelism
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
# I/O-bound: threads
with ThreadPoolExecutor(max_workers=20) as ex:
futures = {ex.submit(fetch, url): url for url in urls}
for fut in as_completed(futures):
url = futures[fut]
try: data = fut.result()
except Exception as e: log.warning("%s failed: %s", url, e)
else: process(data)
# CPU-bound: processes
with ProcessPoolExecutor() as ex:
for result in ex.map(heavy_compute, items, chunksize=100):
...Phase 7.3 covers when to use threads vs processes vs async.
12. dataclasses, enum, typing, abc
(Already covered in Phase 2-3.) Keep them in your fingertip toolkit.
from enum import Enum, auto, IntEnum, StrEnum # StrEnum: 3.11+
class Color(Enum):
RED = auto()
GREEN = auto()
BLUE = auto()
class HttpStatus(IntEnum):
OK = 200
NOT_FOUND = 404
class Mode(StrEnum):
READ = "r"
WRITE = "w"
Color.RED.name, Color.RED.value
HttpStatus(404) is HttpStatus.NOT_FOUND
"r" == Mode.READ # True (StrEnum is also str)13. Less common but worth knowing
| Module | Use |
|---|---|
bisect | Sorted-list operations (bisect_left, insort) — O(log n) |
heapq | Heap-based priority queue |
queue | Thread-safe queues; producer/consumer |
csv | CSV reader/writer (Lesson 1.4) |
statistics | mean, median, stdev (small data; use NumPy for big) |
fractions, decimal | Exact arithmetic |
math | sqrt, log, factorial, comb, isclose |
urllib.parse | URL parsing (use over hand-coded splits) |
mimetypes | Guess content type from extension |
gzip, bz2, lzma, zipfile, tarfile | Compression / archives |
pickle, marshal, shelve | Serialise Python objects (security caveats) |
weakref | References that don't prevent GC |
gc | Manual garbage collection control |
contextvars | Async-safe thread-local variables |
pickle and shelve deserialise arbitrary Python — never load untrusted data with them. Use JSON / msgpack instead.
14. Worked example: a tiny "find duplicate files" script
import hashlib
from collections import defaultdict
from pathlib import Path
def hash_file(p: Path, chunk: int = 65536) -> str:
h = hashlib.blake2b(digest_size=16)
with p.open("rb") as f:
while data := f.read(chunk):
h.update(data)
return h.hexdigest()
def find_duplicates(root: Path) -> dict[str, list[Path]]:
buckets: dict[str, list[Path]] = defaultdict(list)
for p in root.rglob("*"):
if p.is_file():
buckets[hash_file(p)].append(p)
return {h: ps for h, ps in buckets.items() if len(ps) > 1}
if __name__ == "__main__":
import sys
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
for h, paths in find_duplicates(root).items():
print(h)
for p in paths: print(" ", p)pathlib, hashlib, collections, the walrus operator — all stdlib, idiomatic.
Hands-on lab (2 hours)
- Write a CLI that takes a directory and prints the top-10 largest files (
Path.rglob,sort,os.path.getsizeorPath.stat().st_size). - Use
Counterto compute word frequencies of a text file; ignore stopwords. - Convert timestamps from one timezone to another using
zoneinfo. - Spawn
git statusviasubprocess.run; parse the output. - Use
concurrent.futures.ThreadPoolExecutorto fetch 20 URLs in parallel; compare to sequential. - Generate a secure random token using
secrets.token_urlsafe. - Build a
find_duplicates(from the worked example); test on your~/Downloads.
Common pitfalls
- Naive datetimes (no tzinfo). Bugs in DST transitions.
random.choicefor security — usesecrets.subprocess.run(..., shell=True)with user input — shell injection.pickle.loadon untrusted data — RCE.os.path.joineverywhere; switch topathlib.- Forgetting
text=Truein subprocess (returns bytes by default).
Self-check
- What does
itertools.batcheddo? - Difference between
secretsandrandom. - Why is
time.monotonicbetter thantime.timefor measuring intervals? - When use threads vs processes (high level)?
- Why is naive datetime dangerous?
References
- Python standard library docs: https://docs.python.org/3/library/.
- Python Cookbook, Beazley & Jones.
- David Beazley, Python Essential Reference.
- Doug Hellmann, "PyMOTW" (Python Module of the Week).
Sign in to save your progress and earn badges.