Skip to content

Course · Pi · Getting started

Ship your first fix with Pi

Now the ticket. The lead from the last lesson held up: dates flow through stash/feeds.py (stash is the course’s running example, a small FastAPI feed reader) into parse_date() in stash/extract.py, and the failing test - tests/test_extract.py::test_parse_date_preserves_offset - pins what “fixed” means. It feeds the function Mon, 03 Feb 2026 09:00:00 +0530 and expects a datetime that still carries its offset; today the function returns None. Describe the outcome the way you’d hand a ticket to a teammate, and let the loop - Pi picks a tool, runs it, reads what comes back, and repeats until a reply needs no tool - do the legwork, knowing nothing will stop and ask you first.

Here’s the claim to check once you’ve read the two transcripts below: the whole fix closes in five tool calls - two reads, one edit, two pytest runs - and not one of them pauses to ask whether it should proceed. Count them as you go.

> articles with timezone-offset dates are sorting into the wrong place
in my reading queue - the date parse silently returns None for them.
There's a failing test pinning the behavior I want. Make it pass
without weakening the test, run the tests to show me where we
stand, and don't fix anything else today.

You didn’t say how to fix it, didn’t paste code, didn’t pick formats. The one instruction you did set - don’t weaken the test - is what keeps “make it pass” honest; it’s guidance the model weighs, not an enforced gate, which is exactly why you’ll read the diff in a moment. Pi runs the loop from the last lesson to close the gap itself; the shape of the session looks like this:

read: stash/extract.py
read: tests/test_extract.py
Found it. parse_date() tries two strptime formats,
"%a, %d %b %Y %H:%M:%S" and "%Y-%m-%dT%H:%M:%S" - neither has %z,
so "Mon, 03 Feb 2026 09:00:00 +0530" dies with "unconverted data
remains" and falls through to None. The test wants the offset
kept. I'll put two offset-aware formats ahead of the naive ones
and change nothing else.
edit: stash/extract.py
- formats = ["%a, %d %b %Y %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
+ formats = [
+ "%a, %d %b %Y %H:%M:%S %z",
+ "%Y-%m-%dT%H:%M:%S%z",
+ "%a, %d %b %Y %H:%M:%S",
+ "%Y-%m-%dT%H:%M:%S",
+ ]
bash: pytest tests/test_extract.py -q
→ 1 passed

Count what happened before you read a single line of the change: two reads, a plan stated in two sentences, one edit, one focused pytest run - four calls, each executed the instant Pi decided on it. The verify step ran because the ticket asked for it. By the time you caught up, the fix was already on disk and already green.

Read the diff - this is the beat you have to supply yourself

Section titled “Read the diff - this is the beat you have to supply yourself”

On an agent with an approve gate, the pause before “apply” is where you’d look closely. Pi doesn’t give you that pause, so the same discipline moves to after the fact:

> show me everything you changed

You’re reading for the two things you’d check anywhere - does this do what the ticket asked, and did it touch anything the ticket didn’t mention. Here the read is small: one list in one function. The two original formats are still present, two offset-aware ones now sit ahead of them, and nothing else in the file moved.

One detail worth noticing while you’re there: %z also accepts a trailing Z, the shorthand Atom feeds use for UTC, so the second new format covers ISO dates that end in one. That comes back in the next chapter.

Read the diff every time regardless of how small the change looks. The next fix you hand Pi might not be this contained, and nothing in the loop will warn you either way.

Run the whole suite before you call it done

Section titled “Run the whole suite before you call it done”

The focused run tells you your test passes; it says nothing about the rest of the repo. You inherited two failing tests when you took the project on, and this ticket only ever claimed one of them, so ask for the wider run yourself - an agent’s scope rarely grows beyond its ticket unless you widen it:

> now run the whole suite
bash: pytest -q
→ 1 failed, 1 passed

The failure is tests/test_feeds.py::test_fetch_feed_parses_entries, the second inherited failure, the one the README never mentions. It hands fetch_feed() a sample Atom feed containing one entry and asserts one article comes back; zero do. Your fix neither caused nor cured it - the baseline was two failures, exactly one flipped, nothing new appeared. That comparison, full suite after versus baseline before, is what “done” honestly means on a codebase you just inherited.

Why not chase it now? It’s a different function, fetch_feed() in stash/feeds.py, and likely a different species of bug - working out what’s actually wrong with it is the next chapter’s opening job. The disciplined move at this moment is to stop while the working tree contains exactly one reviewed change, and put a floor under it.

> commit this with a sensible message
bash: git add stash/extract.py && git commit -m "Preserve timezone offsets when parsing feed dates"
→ [main a91f2c3] Preserve timezone offsets when parsing feed dates
1 file changed, 6 insertions(+), 1 deletion(-)

Commit now, while the tree holds exactly the change you just read - and stage that file by name rather than reaching for git commit -a, which would sweep in every other tracked modification you haven’t reviewed. There is no approve gate holding your next instruction back, so the next thing you type could as easily be a mistake as this was a fix - and nothing built into the loop will stop it from landing.

A commit at every reviewed, working checkpoint is the one floor that doesn’t move. However badly the next experiment goes - including tomorrow’s visit to fetch_feed() - the cost is capped at whatever changed since the last commit. On a codebase you’ve had for exactly one hour, that floor is doing a lot of the work “trust” would otherwise have to do.

You shipped a real fix - described in plain English, closed in five tool calls, verified against the inherited baseline, read as a diff, committed - and Pi never once asked your permission. That should feel like a capability and a small vertigo at the same time. It worked, but you watched it from the outside: you saw the parts it chose to print, not why it made the calls it made along the way.

One inherited failure is still red, and it’s the first thing the next chapter picks up. Before you go after it, open the box: the next chapter opens the box on those tools - the three you just watched plus the fourth this page never fired - and the same loop, with every read, every token, and every decision sitting somewhere you can go look at instead of narrated after the fact. Next: see every read and every decision.