What you'll learn
Python ships "batteries included" — a huge standard library you can use without installing anything. This lesson tours the modules you'll reach for again and again: specialized containers, iterator tools, system access, running commands, building CLIs, and proper logging.
By the end of this lesson you'll be able to:
- Use
Counter,defaultdict, anddequefromcollections - Combine and slice iterators with
itertoolsand fold withreduce - Read environment and platform info with
os,sys, andplatform - Run external commands with
subprocessand build CLIs withargparse - Replace
print-debugging with reallogging
collections
The collections module adds container types that turn common chores into one-liners: Counter tallies, defaultdict removes KeyError boilerplate, and deque is a double-ended queue with fast operations at both ends.
from collections import Counter, defaultdict, deque
# Counter: tally things instantly
votes = ["a", "b", "a", "c", "a", "b"]
print(Counter(votes)) # Counter({'a': 3, 'b': 2, 'c': 1})
print(Counter(votes).most_common(1)) # [('a', 3)]
# defaultdict: a missing key auto-creates its default (here, [])
groups = defaultdict(list)
for name in ["Ada", "Al", "Bea"]:
groups[name[0]].append(name) # bucket by first letter
print(dict(groups))
# deque: fast appends/pops at BOTH ends
q = deque([1, 2, 3])
q.appendleft(0)
q.append(4)
print(q)itertools & functools
itertools is a set of building blocks for iterators — lazy, memory-efficient, and composable. functools complements it with tools for functions (you met reduce, lru_cache, and wraps earlier).
from itertools import chain, combinations, islice, count
from functools import reduce
print(list(chain([1, 2], [3, 4]))) # [1, 2, 3, 4]
print(list(combinations("ABC", 2))) # [('A','B'), ('A','C'), ('B','C')]
# reduce: fold a sequence into a single value
print(reduce(lambda a, b: a * b, [1, 2, 3, 4])) # 24
# islice takes a slice of any (even infinite) iterator
print(list(islice(count(10, 5), 4))) # [10, 15, 20, 25]Tip
itertools functions return lazy iterators, so wrap them in list() to see the values. That laziness is the point: islice(count(...), 4) works even though count is infinite.os, sys & platform
These modules connect your program to the machine it runs on: os for environment variables and the filesystem, sys for the interpreter and arguments, platform for OS details.
import os, sys, platform
print(sys.version_info[:2]) # e.g. (3, 12) -> Python version
print(platform.system()) # 'Windows' | 'Linux' | 'Darwin'
os.environ["APP_MODE"] = "dev" # set an environment variable
print(os.environ.get("APP_MODE")) # -> dev
print(os.environ.get("MISSING", "fallback")) # -> fallback (safe default)Note
os.environ.get("KEY", default) rather than hard-coding them — the same code then runs in dev, test, and production with different settings.subprocess & argparse
subprocess.run launches other programs and captures their output — the Pythonic way to shell out. Pass arguments as a list, not one big string, to stay safe from shell injection.
import subprocess
# Run an external command and capture its output
result = subprocess.run(
["python", "--version"],
capture_output=True,
text=True, # decode bytes to str
)
print(result.returncode) # 0 means success
print(result.stdout.strip()) # e.g. Python 3.12.0argparse turns a script into a real command-line tool, with positional arguments, optional flags, type conversion, defaults, and an automatic --help:
# greet.py
import argparse
parser = argparse.ArgumentParser(description="Greet someone.")
parser.add_argument("name") # required positional
parser.add_argument("--times", type=int, default=1) # optional flag
args = parser.parse_args() # reads sys.argv
for _ in range(args.times):
print(f"Hello, {args.name}!")$ python greet.py Ada --times 2
Hello, Ada!
Hello, Ada!
$ python greet.py --help
usage: greet.py [-h] [--times TIMES] name
Greet someone.logging
For anything beyond a quick script, replace print with logging. You get severitylevels, timestamps, and the ability to route messages to files or services — all controllable without touching your code.
import logging
logging.basicConfig(
level=logging.INFO, # show INFO and above
format="%(levelname)s: %(message)s",
)
logging.debug("verbose detail") # hidden: below the INFO threshold
logging.info("app started")
logging.warning("low disk space")
logging.error("connection failed")Key idea
print: you can raise the level to WARNING in production and the noisy debug/info calls simply go quiet — no edits, no redeploy of changed code.Recap & quick check
Key takeaways
- collections adds Counter (tally), defaultdict (no KeyError), and deque (fast at both ends).
- itertools gives lazy, composable iterator tools (chain, combinations, islice); functools has reduce, lru_cache, wraps.
- os/sys/platform expose environment variables, interpreter info, and OS details.
- subprocess.run launches external programs — pass args as a list to avoid shell injection.
- argparse builds real CLIs with positional args, optional flags, defaults, and automatic --help.
- logging replaces print with levels, timestamps, and configurable routing you can change without editing code.
Quick check
1. Which collections type tallies how often each item appears?
2. Why wrap itertools results in list()?
3. How should you pass arguments to subprocess.run to avoid shell injection?
4. What does argparse give you automatically?
5. What's the main advantage of logging over print?
Next we tackle one of Python's most talked-about topics: doing many things at once, and the famous Global Interpreter Lock. Next up: Module 30 — Concurrency: Threads, Processes & the GIL.