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.

📦 Module 4 9 min read Not started

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

  1. Know the 20 modules you'll actually use.
  2. Use collections and itertools idiomatically.
  3. Manipulate dates and times correctly (timezones!).
  4. Spawn subprocesses safely.
  5. Use OS, env, paths, temp files, hashing.

1. The "must-know" 20

ModuleWhat
os, os.pathOS interaction; mostly replaced by pathlib for paths
sysInterpreter introspection, argv, stdin/out/err
pathlibObject-oriented paths
ioIn-memory streams; file abstraction
tempfileTemp files / dirs that clean up
shutilHigh-level file ops (copy, move, rmtree, archive)
globFile pattern matching (or Path.glob)
reRegex
collectionsCounter, defaultdict, OrderedDict, deque, ChainMap, namedtuple
itertoolsIterator combinators
functoolspartial, lru_cache, cache, reduce, wraps, cached_property, singledispatch
datetime, zoneinfoDates, times, timezones
timeSleep, monotonic clock, perf counter
json, csv, tomllibCommon formats
subprocessRun external commands
argparse (or typer, click)CLI args
loggingLogs
secrets, hashlib, hmacCrypto-grade randomness, hashing
concurrent.futuresThread/Process pools
asyncioAsync 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

python
from collections import Counter, defaultdict, deque, OrderedDict, ChainMap, namedtuple

(Recap from Lesson 1.2. Key idioms:)

python
# 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

python
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

python
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:)

python
@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 rest

5. datetime, zoneinfo, time

python
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

python
import time
time.sleep(0.5)                               # seconds
time.monotonic()                              # for measuring intervals
time.perf_counter()                           # highest precision; for benchmarking

Don't use time.time() to measure durations — the wall clock can jump (NTP corrections).


6. os, os.environ, sys

python
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            # streams

For env loading with defaults / types, use pydantic-settings (Lesson 2.3).


7. subprocess — run external commands

python
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

python
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

python
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

python
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

python
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.

python
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

ModuleUse
bisectSorted-list operations (bisect_left, insort) — O(log n)
heapqHeap-based priority queue
queueThread-safe queues; producer/consumer
csvCSV reader/writer (Lesson 1.4)
statisticsmean, median, stdev (small data; use NumPy for big)
fractions, decimalExact arithmetic
mathsqrt, log, factorial, comb, isclose
urllib.parseURL parsing (use over hand-coded splits)
mimetypesGuess content type from extension
gzip, bz2, lzma, zipfile, tarfileCompression / archives
pickle, marshal, shelveSerialise Python objects (security caveats)
weakrefReferences that don't prevent GC
gcManual garbage collection control
contextvarsAsync-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

python
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)

  1. Write a CLI that takes a directory and prints the top-10 largest files (Path.rglob, sort, os.path.getsize or Path.stat().st_size).
  2. Use Counter to compute word frequencies of a text file; ignore stopwords.
  3. Convert timestamps from one timezone to another using zoneinfo.
  4. Spawn git status via subprocess.run; parse the output.
  5. Use concurrent.futures.ThreadPoolExecutor to fetch 20 URLs in parallel; compare to sequential.
  6. Generate a secure random token using secrets.token_urlsafe.
  7. Build a find_duplicates (from the worked example); test on your ~/Downloads.

Common pitfalls

  1. Naive datetimes (no tzinfo). Bugs in DST transitions.
  2. random.choice for security — use secrets.
  3. subprocess.run(..., shell=True) with user input — shell injection.
  4. pickle.load on untrusted data — RCE.
  5. os.path.join everywhere; switch to pathlib.
  6. Forgetting text=True in subprocess (returns bytes by default).

Self-check

  1. What does itertools.batched do?
  2. Difference between secrets and random.
  3. Why is time.monotonic better than time.time for measuring intervals?
  4. When use threads vs processes (high level)?
  5. 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.