Month 03 — Python Fluency
Phase: Foundations
Overview
For two months you have been a power user of other people’s programs. In Month 1 you learned the command line and Git; in Month 2 you learned to speak HTTP and JSON, poking at APIs with curl, httpie, and jq. You have been running commands. This month you start writing programs.
Python is the choice for one reason above all: it is the lingua franca of the AI ecosystem. Nearly every model SDK, agent framework, and tutorial you touch for the rest of this course speaks Python first. Learning it now means the agentic work in Months 4 through 12 is about ideas, not syntax. We spend a whole month on the language before any of it touches a network, because an agent is ultimately a Python program in a loop — and you cannot write that loop until you can write a program.
This is a “the language itself” month: no HTTP calls, no API keys, no AI. That is intentional. Month 4 reintroduces the network from inside your own code. By keeping the network out, we remove a whole category of confusing failure (Is it my code? The network? The API?) so that when a Traceback appears, it is your logic — and you learn to read it without fear.
The throughline is the standard library. Python ships with batteries: JSON, CSV, file paths, argument parsing, dates — all built in. You will lean on it hard, add exactly one external tool (uv) to manage your projects, and finish by shipping a real multi-tool command-line application you would actually use.
Here is the mental model for what “running a program” actually does — the loop you will repeat hundreds of times this month:
flowchart LR
A["You write hello.py"] --> B["uv run python hello.py"]
B --> C["Interpreter reads it"]
C --> D{"Valid Python?"}
D -->|No| E["Traceback to stderr"]
D -->|Yes| F["Output to stdout"]
E --> A
F --> A
Notice: the interpreter only ever does one of two things — print output, or print a traceback. A traceback is feedback, not failure, and it sends you right back to the editor.
Prerequisites
Coming in, you should be able to do everything from Months 1 and 2:
- Navigate the filesystem, create and edit files, and run commands in zsh on macOS (Month 1).
- Use Git locally and push to GitHub:
init,add,commit,push, branches (Month 1). - Read and reason about HTTP and JSON — you know what a JSON object, array, string, and number look like, and you can pretty-print JSON with
jq(Month 2). - Install software with Homebrew (
brew install ...).
You do not need any prior programming experience. This is your first language.
Warm-Up: Retrieve Before You Begin
Before reading on, answer these from memory — no peeking at earlier months. This pulls forward the prior skills this month builds on.
- In the shell, how do you run a script you wrote, and how is that different from typing commands one at a time? (Month 1)
- What does a JSON object look like, and what does a JSON array of objects look like? Sketch one of each. (Month 2)
- When you piped a command into
jqin Month 2, which stream carried the data —stdoutorstderr? (Months 1–2) - What command creates a Git commit, and what does a commit actually capture? (Month 1)
Check your recall
1. You save commands in a file and run it (e.g. `./script.sh` or `bash script.sh`); a script is repeatable and version-controlled, whereas typed commands vanish. (Month 1) This is the exact leap you make again here, from the REPL to a `.py` file. 2. An object is `{"name": "Ada", "age": 36}` — keys mapped to values; an array of objects is `[{"name": "Ada"}, {"name": "Grace"}]`. (Month 2) That array-of-objects shape becomes a Python **list of dicts** this month. 3. `stdout` carried the data; `stderr` is for errors and diagnostics. Keeping them separate is what let piping work. (Months 1–2) Your tools this month preserve that split. 4. `git commit -m "..."` records a snapshot of the staged changes with a message. (Month 1) You will commit each lab.Learning Objectives
By the end of this month you can:
- Write and run a Python program from a file as a real, repeatable command, not just lines typed into a REPL.
- Choose the correct built-in data structure (list, dict, set, tuple) for a given problem and explain the tradeoff.
- Implement control flow and functions, and refactor repeated logic into reusable functions with clear inputs and outputs.
- Transform data with comprehensions where they improve clarity over a loop.
- Read and write files, and round-trip data through JSON and CSV using only the standard library.
- Handle errors deliberately with
try/except/raise, distinguishing recoverable from fatal conditions. - Build a command-line tool that accepts arguments via
argparseand behaves like a well-mannered Unix program (exit codes,stderr,--help). - Manage a Python project with
uv: create it, add dependencies, run it, and read itspyproject.toml. - Read a
Tracebacktop-to-bottom and fix the line it points to without guessing.
Tech Stack (free, macOS)
| Tool | Install | Why |
|---|---|---|
| Python 3.12+ | uv python install 3.12 (uv manages it) |
The language. We never use the system Python directly. |
| uv | brew install uv |
Fast, modern project + environment + version manager. One tool replaces python -m venv, pip, and pyenv. |
| VS Code | brew install --cask visual-studio-code |
Free editor. Install the official Python extension for inline errors and the Run button. |
| Git + GitHub | from Month 1 | You will commit each lab and push the milestone repo. |
No paid services, no API keys, no LLM access is required this month. Everything runs offline on Apple Silicon or Intel.
A note on uv over venv/pip: the older path (python -m venv .venv, source .venv/bin/activate, pip install) still works and you should recognize it. But uv is faster, manages Python versions as well as packages, and produces a clean pyproject.toml and lockfile by default — which is exactly the project hygiene the rest of this course assumes. We teach uv as the canonical toolchain and explain the pip concepts underneath so the vocabulary transfers.
Weekly Breakdown
Budget ~8–12 hours per week: roughly half reading and typing along, half doing the lab.
Week 1 — The language core
Warm-start (do this first): before any new material, open the last shell script you wrote in Month 1, re-run it, and read it line by line. Notice it is a file of commands you run on demand — exactly what a .py program is. You are about to write the same idea in a more powerful language.
Focus: going from REPL to a runnable file; the primitive types and control flow.
Topics: the REPL vs. a .py file; variables and dynamic typing; int, float, str, bool, None; f-strings; if/elif/else; for and while; range; truthiness; def functions, parameters, return values, and default arguments.
Reading: Core Concepts §1–§3 below; the official tutorial sections 3–4.
Build: Lab 1, part 1 — small programs that compute and branch.
Week 2 — Data structures and comprehensions
Focus: the four core collections and when each is the right tool.
Topics: list (ordered, mutable), dict (key→value lookup), set (uniqueness, membership), tuple (fixed, immutable records); indexing and slicing; iterating with enumerate and items(); list/dict/set comprehensions; nesting (a list of dicts is the shape of tabular data).
Reading: Core Concepts §4–§5.
Build: Lab 1, part 2 — transform and summarize a dataset held in memory.
Week 3 — Files, JSON, CSV, and errors
Focus: programs that read the outside world and survive bad input.
Topics: pathlib.Path; opening files with with; reading and writing text; the json module (load/dump, loads/dumps); the csv module (DictReader/DictWriter); the exception model; try/except/else/finally; raising your own errors; recoverable vs. fatal.
Reading: Core Concepts §6–§7.
Build: Lab 2 — a JSON/CSV round-trip tool that does not crash on missing or malformed files.
Week 4 — Real programs and the Toolbelt
Focus: turning scripts into installable command-line tools and packaging them.
Topics: if __name__ == "__main__"; argparse (positional args, options, --help, types, defaults); exit codes and sys.stderr; project layout with uv init; uv add, uv run, and reading pyproject.toml; console_scripts entry points.
Reading: Core Concepts §8–§9.
Build: Lab 3 — the Toolbelt milestone: three CLI utilities in one packaged repo.
Core Concepts
§1 — The REPL, the file, and “running a program”
Open a Python prompt by typing uv run python in a project (more on projects later) or just python3 to experiment. This is the REPL — Read, Evaluate, Print, Loop. Type 2 + 2, press return, and it prints 4. The REPL is a calculator with the full language behind it; it is where you explore. But a REPL session vanishes when you close it.
A program lives in a file. Put this in hello.py:
name = "Ada"
print(f"Hello, {name}!")
Run it with python3 hello.py. The difference is everything: a file is repeatable, version-controllable, and shareable. From here on, “write a program” means “put code in a .py file and run it.” The REPL is for trying a single expression to see what it does.
print() writes to the screen. The f"..." is an f-string — Python substitutes {name} with the value of the variable. F-strings are the modern, readable way to build text and you will use them constantly.
§2 — Variables and types
name = "Ada" binds the name name to a string value. Python is dynamically typed: you never declare a type, and a name can be rebound to a different type later. But values absolutely have types, and mixing them wrongly is an error you will see often:
age = 11 # int
height = 1.45 # float
name = "Ada" # str
is_student = True # bool
nothing = None # the absence of a value
None is special: it means “no value here yet” and is what a function returns if you do not return anything. The classic beginner trap is the gap between a string and a number: "11" + 1 raises a TypeError, because you cannot add text to a number. Convert explicitly with int("11") or str(11). The why: Python refuses to guess what you meant, which feels strict but prevents whole categories of silent bugs.
Common misconception.
=and==are interchangeable ways to compare values. Reality.=assigns (age = 11binds the name);==compares (age == 11asks a true/false question). The single=feels like the math “equals,” which is exactly why it is tempting — but in Python they are different operators, and using=where you meant==is aSyntaxErrorinside anif.
§3 — Control flow and functions
Decisions use if/elif/else; repetition uses for and while. Indentation — four spaces — is not cosmetic in Python; it is the block structure.
Common misconception. Indentation in Python is just style, like it is in most languages — the program would run the same if I lined things up differently. Reality. Indentation is the syntax: it defines which lines belong to a block. Move a line in or out by four spaces and you change what the program does (or get an
IndentationError). It is tempting to treat whitespace as cosmetic because that is true in C or JavaScript, where braces{}mark blocks — Python uses the whitespace itself.
def grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
else:
return "needs work"
for s in [95, 82, 60]:
print(s, "->", grade(s))
A function packages logic behind a name with inputs (parameters) and an output (return). The moment you write the same few lines twice, that is the signal to make a function. Functions are the first real tool of software engineering: they let you name an idea, test it in isolation, and reuse it. Give them verb-like names (grade, load_notes, convert) and keep each one doing one thing.
A subtlety worth learning early: truthiness. if items: is true when the list is non-empty and false when empty; 0, "", [], {}, and None are all “falsy.” This is idiomatic Python — prefer if not items: over if len(items) == 0:.
§4 — The four core data structures
Heavy concept ahead. Slow down here; this is the load-bearing idea of the month. Choosing the right container — and understanding that some of them can change in place while others cannot — underlies every lab and every program you write after this.
This is the heart of the month. Picking the right container is most of what makes code clean.
list— an ordered, changeable sequence:["a", "b", "c"]. Use it when order matters and you will add or remove items. Index from zero (items[0]), slice (items[1:3]), append (items.append(x)).dict— a key→value map:{"name": "Ada", "age": 11}. Use it when you look things up by name rather than by position. This is Python’s workhorse and, not coincidentally, the in-memory shape of a JSON object.set— an unordered collection of unique things:{"a", "b"}. Use it for membership tests (x in seen) and for de-duplication. Asking “have I seen this before?” is a set’s whole job, and it answers fast.tuple— an ordered, fixed sequence:(40.7, -74.0). Use it for a small fixed record that should not change, like a coordinate pair. Because it is immutable, it can be a dict key where a list cannot.
The decision rule: order + change → list; lookup by name → dict; uniqueness/membership → set; fixed record → tuple. Most real data ends up as a list of dicts — exactly what csv.DictReader and a JSON array of objects give you, which is why this shape recurs all month.
The split that trips up every beginner is mutability — whether a value can be changed in place after it is created. A list, dict, and set are mutable (you can append, reassign a key, add a member); a tuple, str, int, and bool are immutable (any “change” actually makes a new value). This is why a tuple can be a dict key and a list cannot, and it is the source of a whole class of surprising bugs.
flowchart TD
A["You have a value"] --> B{"Mutable type?"}
B -->|"list / dict / set"| C["Changing it in place affects every name pointing at it"]
B -->|"tuple / str / int"| D["Any change makes a brand-new value"]
C --> E["Two names can share one list — edit one, both see it"]
D --> F["Safe to use as a dict key; never changes underfoot"]
Notice: with mutable types, b = a makes two names for the same object — a.append(1) shows up in b too. With immutable types you never get that surprise.
Common misconception. A list and a tuple are basically the same thing — just different brackets — so they are interchangeable. Reality. They look alike (
[1, 2]vs(1, 2)), which is exactly why this is tempting. But a list is mutable and a tuple is immutable: you canappendto a list, never to a tuple, and only a tuple can be adictkey or live in aset. Reach for a tuple when the record is fixed (a coordinate, a row that must not change); reach for a list when you will grow or reorder it.
§5 — Comprehensions
A comprehension builds a collection from another collection in one expressive line:
nums = [1, 2, 3, 4, 5]
squares = [n * n for n in nums] # [1, 4, 9, 16, 25]
odds = [n for n in nums if n % 2] # [1, 3, 5]
by_square = {n: n * n for n in nums} # {1: 1, 2: 4, ...}
Read it left-to-right as “give me n*n for each n in nums.” Comprehensions are idiomatic and often clearer than the equivalent for loop with .append(). The why and the caution: use them when the transformation is simple. If you need multiple steps, nested conditions, or side effects, a plain loop is more readable — clarity beats cleverness.
§6 — Files, JSON, and CSV
Programs become useful when they read and write the outside world. Use pathlib for paths and the with statement for files — with guarantees the file is closed even if an error occurs:
from pathlib import Path
note = "remember to commit"
Path("notes.txt").write_text(note + "\n")
print(Path("notes.txt").read_text())
For structured data, the standard library has you covered — no installs needed. JSON maps directly onto Python: object→dict, array→list, string→str, number→int/float, true→True, null→None.
import json
data = {"tasks": ["a", "b"], "done": False}
text = json.dumps(data, indent=2) # Python -> JSON string
back = json.loads(text) # JSON string -> Python
dump/load work on file objects; dumps/loads work on strings (the s is for string). CSV is rows of cells; csv.DictReader turns each row into a dict keyed by the header — the list-of-dicts shape again — and DictWriter writes it back. This JSON↔CSV round-trip is the core of Lab 2.
§7 — Errors: recoverable vs. fatal
A Traceback is not a scolding; it is a map. Read it bottom-up: the last line names the exception and message (ValueError: invalid literal for int()), and the lines above show exactly which line in which file raised it. Most “I’m stuck” moments dissolve once you actually read that bottom line.
You handle expected failures with try/except:
try:
count = int(user_value)
except ValueError:
print("That wasn't a number; using 0.", file=sys.stderr)
count = 0
The discipline: catch specific exceptions you can recover from, not a bare except: that swallows everything (including bugs and Ctrl-C). A missing optional file might be recoverable — fall back to a default. A required file that is corrupt is fatal — print a clear message and exit non-zero. You can also raise ValueError("explain what's wrong") to signal a problem you cannot handle locally. Good error handling is the difference between a tool that crashes with a wall of red and one that says “I couldn’t read config.json — is it valid JSON?” and exits cleanly.
The full shape — try/except/else/finally — routes control depending on whether the risky code raised:
flowchart TD
A["try: run risky code"] --> B{"Did it raise?"}
B -->|Yes| C["except: handle the error"]
B -->|No| D["else: ran cleanly"]
C --> E["finally: always runs"]
D --> E
E --> F["continue program"]
Notice: else runs only on success, except only on failure, but finally runs either way — it is for cleanup you must not skip.
§8 — A script that behaves like a real program
Two conventions turn a script into a tool. First, the guard:
def main():
...
if __name__ == "__main__":
main()
__name__ is "__main__" only when the file is run directly, not when it is imported. This lets a file be both a runnable program and a reusable module — a pattern every Python project uses.
Second, argparse gives you arguments, types, defaults, and a --help screen for free:
import argparse
def main():
p = argparse.ArgumentParser(description="Greet someone")
p.add_argument("name")
p.add_argument("--count", type=int, default=1)
args = p.parse_args()
for _ in range(args.count):
print(f"Hi {args.name}")
if __name__ == "__main__":
main()
A well-mannered Unix tool also uses exit codes (sys.exit(1) on failure) and writes errors to stderr, not stdout, so that piping (mytool | jq from Month 2) keeps clean data on stdout. These habits make your tools composable with everything you learned in Months 1 and 2.
§9 — Projects with uv
A real project is more than a loose file. uv scaffolds one:
uv init toolbelt && cd toolbelt
uv add rich # adds a dependency, writes it to pyproject.toml, locks it
uv run python -m toolbelt --help
uv init creates pyproject.toml — the manifest that declares your project’s name, Python version, and dependencies — plus an isolated virtual environment so your project’s packages never collide with another project’s. uv add is the modern pip install that also records the dependency in the manifest (the old way, pip install rich, installed it but forgot it the moment you closed the terminal). uv run executes a command inside that environment. Reading a pyproject.toml and understanding its [project] and dependencies sections is a skill you will use in every Python repo for the rest of the course.
Labs
| Lab | Title | Time | Difficulty |
|---|---|---|---|
| Lab 1 | Language Core and Data Structures | ~3.5 hrs | Intro |
| Lab 2 | Files, JSON, CSV, and Error Handling | ~3.5 hrs | Core |
| Lab 3 | Build the Toolbelt CLI (Milestone) | ~5 hrs | Core / Stretch |
Checkpoints & Self-Assessment
Run these against yourself at the end of each week. You are on track if you can answer without looking it up.
- Week 1: Without copying, write a
hello.pythat takes no input, defines a function, and prints a branched result. Can you explain why"11" + 1fails? - Week 2: Given a list of dicts (people with names and ages), can you, in the REPL, build a list of just the names and a set of the unique ages — using comprehensions?
- Week 3: Write a five-line program that reads a JSON file and prints one field. Now point it at a file that does not exist. Does it crash with a
Traceback, or print a clean message? Make it the latter. - Week 4: Run
uv run mytool --helpon your own tool and get a sensible help screen. Can you point to the line inpyproject.tomlthat declares a dependency?
Reflect
Spend ten minutes on these in your learning log (writing, not just thinking):
- Explain it back: In two or three sentences, explain the difference between a mutable type (list, dict, set) and an immutable type (tuple, str, int) as if teaching a peer who just finished Month 2.
- Connect: How does writing a
.pyprogram and running it withuv runchange or extend the shell scripts you wrote in Month 1? What can Python do that a shell one-liner gets clumsy at? - Connect: The list-of-dicts shape is the same data you met as a JSON array of objects in Month 2. Where in this month did that shape show up, and why does it keep recurring?
- Monitor: Which concept this month is still fuzzy — comprehensions, mutability,
try/except, orargparse? Name it precisely, and write the one question that would clear it up.
Month-End Assessment
Deliverable: the Toolbelt — a single uv-managed repository containing three command-line utilities, each solving a real problem with clean I/O and graceful error handling, plus a real pyproject.toml and a README.md. The three tools (built in Lab 3):
csv2json— converts a CSV file to JSON (and prints to stdout or a file).dirsize— reports the total size of a directory and its largest entries, human-readable.note— appends a timestamped line to a notes file and can list recent notes.
Each tool: accepts arguments via argparse, has a working --help, reads input, handles errors gracefully (missing file, bad input) with a clear message and non-zero exit code, and prints clean output. The repo is pushed to GitHub.
Rubric
- Passing: All three tools run and produce correct output on valid input. Each has
argparsewith--help. At least one error path (e.g., missing file) is handled without a rawTraceback.pyproject.tomlexists anduv runworks. README explains how to install and run each tool. Repo is on GitHub. - Excellent: All of the above, plus: errors go to
stderrwith non-zero exit codes; tools are composable (output ofcsv2jsonpipes cleanly intojq); code is factored into small functions with amain()guard; no bareexcept:; tools installed as real commands viaconsole_scriptsentry points so you can runcsv2json file.csvdirectly; and a short docstring or comment explains each module’s purpose.
The real definition of done is behavioral: you reach for Python instead of a shell one-liner when the logic gets complex, and a Traceback no longer scares you — you read the bottom line and fix it.
Common Pitfalls
- Editing the system Python. macOS ships its own Python; never
pip installinto it or rely on it. Letuvmanage the version per project. Ifpython3points somewhere surprising, that is why — useuv run python. IndentationErrorand tabs vs. spaces. Python blocks are indentation. Set your editor to insert 4 spaces, never tabs. VS Code’s Python extension does this for you.- Mutable default arguments.
def f(items=[])reuses the same list across calls — a classic bug. Usedef f(items=None)and create the list inside. - Off-by-one and zero-indexing.
items[len(items)]is always anIndexError; the last element isitems[-1]. Slices are half-open:items[1:3]gives indices 1 and 2, not 3. - Bare
except:. It hides real bugs and even swallows Ctrl-C. Always name the exception you expect. - Forgetting the
__main__guard. Without it, code runs the moment a file is imported, causing surprises later when files import each other. - Confusing
=and==.=assigns;==compares. Python will not let you assign inside anif, which catches some of this for you. - Comparing JSON booleans. JSON
true/false/nullbecome PythonTrue/False/None(capitalized). Lowercasetrueis aNameError.
Knowledge Check
Answer from memory first, then check. Questions marked ⟲ are spaced callbacks to earlier months — they are supposed to feel like a stretch.
- State the one-sentence rule for choosing each of the four core data structures (list, dict, set, tuple).
- Predict the output:
a = [1, 2, 3] b = a b.append(4) print(a)Why is the answer surprising, and what concept explains it?
- What is the difference between
=and==, and where would each appear? - Spot the bug:
def f(items=[]): items.append(1); return items. Why does callingf()repeatedly return a growing list? - You read a CSV with
csv.DictReaderand theagefield prints as'36', not36. Why, and how do you fix it? - When should you let a program crash with a
Traceback, and when should you catch the exception and recover? Give one example of each. - Which tool and why: you need to convert a CSV file to JSON for a teammate. Do you reach for
jq, a Python script, or a spreadsheet — and what makes your choice composable? - In
argparse, what doesnargs="?"with adefaultgive you, and what doesnargs="+"give you? - ⟲ In a well-behaved Unix tool, which stream gets the data and which gets the error messages — and why does that split matter when you pipe into
jq? (Months 1–2) - ⟲ Write the JSON for an array of two objects, each with a
nameand anage. What Python type does that become afterjson.loads? (Month 2) - ⟲ What
gitcommands take a brand-new project from “files on disk” to “pushed to GitHub”? (Month 1) - What does
if __name__ == "__main__":protect against, and why does every project use it?
Answer key
1. *order + change* → list; *lookup by name* → dict; *uniqueness/membership* → set; *fixed record* → tuple. 2. Prints `[1, 2, 3, 4]`. `b = a` did not copy the list — both names point at the *same* mutable object, so appending through `b` is visible through `a`. (Mutability / shared references, §4.) 3. `=` assigns a value to a name; `==` compares two values and yields `True`/`False`. `=` appears in statements; `==` appears in conditions like `if x == 1:`. 4. The default `[]` is created *once* when the function is defined and reused on every call, so it accumulates. Fix with `def f(items=None): items = items or []`. 5. CSV has no types — every cell is text. Convert explicitly with `int(row["age"])` (use the recoverable `to_int` pattern to survive bad values). 6. Crash (or exit non-zero) on *fatal* problems you cannot proceed without — e.g. a required input file is missing. Recover on *expected, local* problems — e.g. one row has a non-numeric age; warn and use a default. 7. A Python script, because the logic (typing, error handling, options) outgrows a `jq` one-liner; keep JSON on `stdout` and messages on `stderr` so it pipes cleanly into `jq`. 8. `nargs="?"` makes one optional positional arg that uses the `default` when omitted; `nargs="+"` collects one-or-more values into a list. 9. Data goes to `stdout`, errors/diagnostics to `stderr`. Piping (`tool | jq`) reads `stdout`, so keeping errors off `stdout` means the pipe never gets polluted with non-JSON text. (Months 1–2) 10. `[{"name": "Ada", "age": 36}, {"name": "Grace", "age": 40}]`; it becomes a Python **list of dicts**. (Month 2) 11. `git init`, `git add -A`, `git commit -m "..."`, then create the remote and `git push -u origin main` (or `gh repo create ... --push`). (Month 1) 12. It stops top-level code from running when the file is *imported* (rather than run directly), so a file can be both a runnable program and a reusable module.Further Reading
- The official Python Tutorial — sections 3–9 cover everything this month, authoritatively and for free.
uvdocumentation — the project/version/dependency manager we use as canonical.argparsetutorial — the gentle official intro to command-line arguments.- Python
pathlibdocs — the modern, object-oriented way to handle files and paths. - “Writing a
pyproject.toml” — what each section of the manifest means.
Author’s Notes
We chose uv as canonical over venv/pip for speed and because it unifies version + environment + dependency management — but the README and labs name the pip concepts underneath so the vocabulary transfers to any repo the learner meets. The strict “no network” boundary is a deliberate pedagogical guardrail handing off to Month 4: it isolates “is this my logic?” from “is this the network?” so the learner builds confidence reading tracebacks. The Toolbelt tools are intentionally non-AI but reinforce the Month-1/2 Unix-composability ethic (stdin/stdout/stderr, exit codes, piping into jq), which pays off when these same conventions reappear in agent harnesses later in the course.