Module 12 · ~7 hours · Priority: Medium

Read Code You Didn't Write

The skill the job is actually made of. A repeatable method for tracing one request through an unfamiliar codebase, plus the pull request workflow and your capstone checkpoint.

Why this is the actual job

What
Reading code someone else wrote, without the author available to explain it, and forming an accurate enough picture to change one line safely.
Where
Every job, from your first day. The codebase is already years old and nobody is going to walk you through it line by line.
When
Before every change you ever make in a codebase you did not create — which after this track is nearly all of them.
How
Not by reading. By tracing one concrete request end to end and letting the path show you the structure.

Eleven modules built you something. This one turns that into employability, because the thing a team pays for is not your ability to start a project — it is your ability to become useful inside one that already exists. You will spend far more of your career reading code than writing it, and almost all of that code will be code you did not write and cannot ask the author about, because they left, or it was 2019, or the commit message says "fix".

Here is the wrong instinct, stated plainly, because almost everyone has it. You open the repository, expand the file tree, click the first file, and start reading from the top. Then the second file. You do this for two hours, you understand nothing, and you conclude you are not smart enough for this codebase.

You are not. The method is wrong. A codebase is a graph, not a book. A book is written to be read in order; nothing in a repository was. Files sit next to each other because of a folder convention, not because they run in sequence. The thing that actually has an order is a request — and the request cuts a path through fifteen files, in a specific order, ignoring the other four hundred. That path is the readable part. Everything else is scenery you can visit later, when a request takes you there.

  • You do not need to understand the whole system to change part of it. Nobody on the team understands the whole system either. They understand the paths they have walked. Seniority is mostly having walked more paths.
  • Depth beats breadth on day one. One request understood completely is worth more than forty files skimmed, because the complete trace teaches you the codebase's conventions — and conventions transfer to every other request.
  • The code is the only document that cannot lie. READMEs go stale, comments describe intentions that changed, architecture diagrams describe the system somebody wanted. The code is what actually runs.
  • Your own project is a legitimate first target. The City Guide API is now eleven modules deep and you have forgotten parts of it. That forgetting is the point — you get to practise the method somewhere you can check your answers against ground truth.
Pick your repository now. Any of three works: your own City Guide API; any open-source Python web project you already have on disk or care to clone; or a colleague's repository if you have one. Nothing in this module needs a specific project, an account anywhere, or a network connection beyond the initial clone. If you have no other repository at hand, use your own and do the whole module against it — then repeat the trace on something unfamiliar when you next get the chance.

Orient before you read 15 minutes

What
A fixed checklist that extracts the shape of any repository before you open a single source file.
Where
The repository root. Everything below is answerable from there in under fifteen minutes.
When
The first fifteen minutes with any codebase, every time, including ones you wrote and forgot.
How
Six commands, in this order, each answering one specific question about the system.

Run these in order. Do not deviate, do not open an editor yet, and do not chase anything interesting until the checklist is finished — the value is in the sequence, because each step narrows where the next one looks.

# 1. What does this claim to be, and how do you run it?
cat README.md

# 2. What does it depend on?  <- the single highest-signal file in the repo
cat requirements.txt          # or pyproject.toml, or package.json

# 3. What is the shape? One level deep only. Do not recurse.
ls -la
ls app/ src/ 2>/dev/null

# 4. What actually starts?
cat Dockerfile | grep -i cmd
cat docker-compose.yml | grep -A2 command
grep -rn "FastAPI(" --include=*.py .   # where is the app object created?

# 5. What is being worked on right now?
git log --oneline -20

# 6. Who knows this code?
git log --format=%an | sort | uniq -c | sort -rn | head

What each one actually tells you, because running them without knowing what you are looking for is just typing:

StepThe question it answersWhat to write down
READMEWhat the authors think this is for, and the run commandThe run command. Nothing else yet.
DependenciesThe architecture, before any source fileWeb framework, DB layer, auth library, test runner
Tree, one levelHow the authors divide responsibilityFolder names — they are the codebase's vocabulary
EntrypointThe one function that runs firstFile and line where the app object is built
git log -20What is alive versus abandonedWhich directories appear in recent commits
Author countsWho to ask, and where the risk isTop three names; whether they still work there

Step 2 deserves its own paragraph. Dependencies tell you the architecture before a single line of source does, because a library is a decision someone already made and cannot easily unmake. sqlalchemy plus alembic means an ORM with versioned migrations — so there is a models module and a migrations/ folder, guaranteed. celery plus redis means work happens outside the request, so some endpoints return before the work is done. python-jose or pyjwt means tokens, not sessions. You can predict two thirds of the folder tree from the manifest alone, and you should try to — then check, because where you are wrong is where this codebase is interesting.

The author count command reads oddly the first time. git log --format=%an prints one author name per commit, sort groups identical names together, uniq -c collapses each group to a count, and the second sort -rn orders by that count, largest first. It is the classic Unix pipeline shape and its output looks like this:

$ git log --format=%an | sort | uniq -c | sort -rn | head
  312 Priya Raghavan
   98 Tom Okafor
   41 dependabot[bot]
   12 Your Name
  • A stale README is data, not an obstacle. If the run command fails, you have learned the project changed and nobody updated the docs — which tells you how much to trust every other document in the repo.
  • Read folder names as claims. A repo with routers/, crud/, schemas/ is claiming those are three separate jobs. A repo with one 2,000-line main.py is claiming they are not. Both are real; know which you are in.
  • Recent commits are your map of the living code. A directory nobody has touched in three years is either perfect or dead, and either way it is not where this week's bug is.
  • Do not read source in the first fifteen minutes. The temptation is enormous and it wastes the orientation, because you will read a file whose role you cannot yet place.
In other stacks: this checklist is language-independent, and only the grep target changes. On an Express project step 2 is package.json and you are looking for express, prisma, passport; step 4 greps for app.listen. On Rails, step 2 is the Gemfile, and step 4 is config/routes.rb — which hands you the entire route table in one file, a genuine luxury. The questions do not change. Only the filenames do.

The trace method

What
Following one concrete request from the moment it arrives to the moment a response leaves, recording every file and line it passes through.
Where
Any web codebase in any language. This is the transferable skill of the entire module.
When
Immediately after orientation, and again whenever you are handed a bug in an area you do not know.
How
Six numbered steps, below, producing a file-by-file list with line numbers.

Pick one request. Not a feature, not a module — one HTTP method and one path, with a concrete value in it. GET /places/1. The concreteness matters: "the places endpoints" is a category and categories cannot be traced, but GET /places/1 is a single path through the graph with a beginning and an end.

  1. Find the route. Grep for the path string: grep -rn "places" --include=*.py .. You are looking for a decorator, a route registration, or a router include — something that ties the string /places to a function.
  2. Find the handler. The function directly beneath that decorator. Write down path/to/file.py:line. This is step zero of your trace and everything else hangs off it.
  3. List every function the handler calls. Literally list them, on paper or in the notes file. Usually three to six. Include the ones that arrive as parameters — in FastAPI a Depends(...) argument is a function call that already ran before your handler started, and beginners miss those every time.
  4. Follow each one down until it either touches the database, calls another service, or returns a plain value. Those three are your stopping conditions. When you hit one, come back up and take the next branch.
  5. Note what transforms the data at each hop. This is the part that turns a list of filenames into understanding. At every step ask: what shape came in, what shape went out? JSON text becomes a validated object; an object becomes SQL; rows become ORM instances; instances become a response model; the response model becomes JSON text again.
  6. Write it down as you go, not afterwards. You will not remember line numbers. A trace reconstructed from memory is a guess with a filename attached.

Here is the method applied to a request you already own. Run it against your own City Guide API first — you can check every claim below against your actual files, which is exactly why you should practise here before applying it to a stranger's repository. Your line numbers will differ; the shape should not.

GET /places/1 HTTP/1.1 | v [1] uvicorn / ASGI server (installed lib) accepts the socket, parses HTTP | Module 02 v [2] app object app/main.py:14 app = FastAPI(...) | v [3] middleware stack app/middleware.py:22 request id, timing, log line | Module 10 v [4] router match app/routers/places.py:31 @router.get("/places/{place_id}") | app/main.py:26 app.include_router(places.router) v [5] dependency resolution app/db.py:18 get_db() -> opens a Session | (runs BEFORE handler) app/auth.py:44 current_user() -> decodes the JWT | Modules 05 and 07 v [6] path param validation (FastAPI internals) "1" (str) -> 1 (int) | v [7] handler app/routers/places.py:33 def get_place(place_id, db, user) | v [8] data access app/crud/places.py:12 db.get(Place, place_id) | v [9] SQL (SQLAlchemy emits) SELECT places.id, places.name, ... | FROM places WHERE places.id = 1 v [10] Postgres container returns 0 or 1 row | v [11] ORM instance app/models.py:9 Place(id=1, name='...') | v [12] serialisation app/schemas.py:27 PlaceRead.model_validate(place) | Module 06 - drops fields not on the model v [13] response 200 application/json {"id":1,"name":"...","rating":4} | v [14] session closed app/db.py:21 the finally: block in get_db()

Two things in that diagram catch every beginner. First, steps 5 and 6 happen before your handler body runs — the database session and the authenticated user already exist by the time your first line executes, which is why a broken token produces a 401 from code you never wrote. Second, step 14 happens after your return statement. Code running after a return is not intuitive, and it is where connection-leak bugs live in every framework ever written.

The trace stops being honest the moment you guess. If you cannot find where something is called from, write "UNKNOWN — how does X reach Y?" in the trace and keep going. An honest gap is useful; a plausible-sounding invention is worse than nothing, because you will build on it. Those gaps become your three questions in the next section.

Tools for reading

What
Five tools that answer questions about code you did not write, faster than reading it can.
Where
Your terminal, in the repository. All five are already installed; none need configuring.
When
The moment the trace stalls — a call you cannot find, a line you cannot explain, behaviour you cannot predict.
How
grep to locate, git log -p for history, git blame for context, the test suite for intent, the debugger for truth.
# LOCATE - where does this string appear at all?
grep -rn "places" --include=*.py .        # -r recursive, -n line numbers
grep -rn "def get_place" --include=*.py . # find a definition
grep -rn "get_place(" --include=*.py .    # find the callers - note the paren

# HISTORY - how did this file become what it is?
git log -p app/routers/places.py          # every change to this file, with diffs
git log --oneline app/routers/places.py   # ... just the messages, for a first pass

# CONTEXT - what commit introduced this confusing line, and why?
git blame app/routers/places.py           # every line, with its commit and author
git show a1b2c3d                          # the full commit that line came from

# INTENT - what did the authors believe should happen?
pytest tests/ -q                          # do they even pass?
cat tests/test_places.py                  # an executable description of the API

# TRUTH - what actually happens, right now, with real values?
# put breakpoint() on the first line of the handler, then hit the endpoint

The paren in grep -rn "get_place(" is the whole trick. Without it you get the definition, every import, and every mention in a comment. With it you get call sites, which is what you actually wanted. Learning to grep for the shape of a call rather than a bare name is worth more than any IDE feature.

git blame is for context, never for blame. The name is a historical joke that has done real damage. What it gives you is a line-to-commit mapping, and the commit gives you a message, a date, and usually an issue reference — which is to say, the reason a strange line exists. Nine times out of ten the strange line is strange because it fixes something, and the commit message tells you what. Run git show on the hash and read the message before you touch the line.

The test suite is documentation that cannot go stale, because a stale test fails. tests/test_places.py is an executable description of what the authors believed the code should do — including edge cases they hit in production and never wrote down anywhere else. When a README and a test disagree, the test is right. Read the test names first: a list of test names is a specification, and you can read forty of them in three minutes.

And when reading fails, stop reading. Put breakpoint() at the top of the handler, send the request, and look at the actual values:

# in the handler, temporarily:
def get_place(place_id: int, db: Session = Depends(get_db)):
    breakpoint()            # execution stops here, terminal becomes a prompt
    ...

# then in the terminal running your server:
(Pdb) place_id              # what value actually arrived?
1
(Pdb) type(place_id)
<class 'int'>
(Pdb) db                    # what IS this thing?
<sqlalchemy.orm.session.Session object at 0x104a2b1d0>
(Pdb) w                     # 'where' - print the full call stack. THE trace, for free
(Pdb) n                     # next line
(Pdb) s                     # step INTO the next call
(Pdb) c                     # continue, let the request finish

That w command prints the entire chain of calls that led to this line — the machine handing you a large part of your trace, with real line numbers, in one keystroke. Thirty seconds in a debugger routinely beats an hour of reading, and beginners avoid it purely because it is unfamiliar. Get familiar.

  • Delete every breakpoint() before committing. One left in a handler hangs the server for whoever runs it next, with no error message. Grep for it as part of your pre-commit habit.
  • git log -p on a confusing file is a story. Read it newest-first and stop when it starts making sense. You are looking for the commit where the current shape appeared.
  • Run the tests before you change anything. If they already fail, that is not your fault and you need to know before you start, or you will spend an afternoon debugging someone else's broken test.

Writing the trace up

What
A short markdown file recording the path a request takes, what transforms it, and what you still do not understand.
Where
notes/request-trace.md in your own repository, alongside your module notes.
When
Written as you trace, finished within the same session — the details evaporate overnight.
How
Four parts: the endpoint, the line-numbered path, the transformations, and three questions you cannot answer.
# Request trace: GET /places/1
Repo: city-guide (my own)   Commit: a1b2c3d   Date: 2026-03-14

## Path
1. app/main.py:14           FastAPI() app object created
2. app/main.py:26           include_router(places.router)
3. app/middleware.py:22     request id attached, timer started
4. app/routers/places.py:31 @router.get("/places/{place_id}")
5. app/db.py:18             get_db() dependency - opens Session
6. app/auth.py:44           current_user() - decodes JWT, loads User
7. app/routers/places.py:33 handler get_place() body starts
8. app/crud/places.py:12    db.get(Place, place_id)
9. app/models.py:9          Place ORM instance returned (or None)
10. app/schemas.py:27       PlaceRead validates and drops internal fields
11. app/db.py:21            finally: db.close()  -- runs AFTER the return

## Transformations
- "1" in the URL path        -> int, by the type hint on the handler param
- primary key int            -> SELECT ... WHERE places.id = 1
- row                        -> Place ORM instance
- Place instance             -> PlaceRead (owner_id dropped, tags flattened)
- PlaceRead                  -> JSON bytes, Content-Type application/json

## Three things I cannot yet explain
1. Who calls get_db()? I never call it - FastAPI does, but where?
2. If two requests arrive at once, do they share a Session? What stops them?
3. tests/conftest.py:31 overrides get_db - how does the real one get replaced?

The three questions are the most valuable part of that file, and it is worth being precise about why. A trace is a thing you can reconstruct any time in twenty minutes. A precise list of what you do not understand is not reconstructible, because it is a snapshot of the edge of your knowledge and it moves.

More practically: knowing exactly what you do not understand is the difference between a junior who is stuck and a junior who is useful. Compare two messages to a senior engineer. "I don't get the database stuff" is unanswerable — it demands an hour they do not have and gives them no idea where to start. "I traced GET /places/1. I can see get_db is declared as a dependency at routers/places.py:33, but I cannot find who calls it, and I do not understand how conftest.py:31 replaces it in tests" takes ninety seconds to answer and makes you look like someone worth answering. That second message is the trace file, quoted.

  • A good question names a file and a line. That is the entire difference between a question that gets a real answer and one that gets "have a look at the docs".
  • Write the questions down even when nobody will read them. Half of them answer themselves in the act of writing, precisely because writing forces you to state what you expected.
  • Keep the commit hash in the file. A trace is only true of one version of the code. Six months later the hash is what tells you whether to trust it.

The pull request workflow

What
The loop every team uses to change code: branch, commit, push, propose, review, merge.
Where
GitHub, GitLab, and Bitbucket all implement it, but the mechanic is pure git and works entirely offline.
When
Every change you make on a team, including one-line ones. Committing to the main branch directly is the exception, not the norm.
How
git switch -c, commit, push to a remote, open the request, respond to comments, merge.

Module 00 gave you six git commands and promised branching would come when you had someone to merge with. This is that module. You may still not have a person — so you are going to be both sides of the loop, which is a genuinely useful exercise because it forces you to read your own diff as a stranger would.

Why work happens on a branch. The main branch is the version that is believed to work. If you commit half-finished work onto it, everyone who pulls gets your half-finished work, and there is no way to say "not that bit". A branch is a private line of history with a name, and it lets a change be complete before anyone else has to care about it. It also makes the change reviewable as a unit, which is the thing a pull request actually is: a diff, plus an argument for why it should exist.

# start work
git switch -c trace/places-notes     # create the branch and move onto it
git status                            # confirm: "On branch trace/places-notes"

# ... make your change, then ...
git add notes/request-trace.md
git commit -m "add request trace for GET /places/1"

# publish it
git push -u origin trace/places-notes # -u links local branch to remote branch

# after the review, back on main
git switch main
git merge trace/places-notes
git branch -d trace/places-notes      # delete the merged branch

No remote? Make one. A "remote" is just another git repository that yours knows about, and it can be a folder on your own disk. A bare repository is one with history but no working files — exactly what a server holds. This gives you the complete push-and-pull mechanic with no account, no network, and nothing to sign up for:

cd ~/projects
git clone --bare city-guide city-guide-origin.git   # a local "server"
cd city-guide
git remote add origin ~/projects/city-guide-origin.git
git remote -v                                        # confirm it is registered
git push -u origin trace/places-notes                # a real push, entirely offline

What makes a pull request reviewable. Four things, and they are all about respecting the reviewer's attention:

  • One concern. If the description needs the word "also", split it. A PR that fixes a bug and renames three files forces the reviewer to hold two unrelated arguments in their head at once, and they will approve it without really reading it.
  • Small. Review quality falls off a cliff somewhere around 400 changed lines. Below that you get real comments; above it you get "LGTM", which means nobody read it and you now have false confidence.
  • A description that says what and why, never how. The diff already says how — it is literally the how. What the diff cannot tell anyone is why this change exists and what would break without it. Two sentences beats two paragraphs.
  • Green tests before you ask. Asking someone to review code you have not run is asking them to do your job. Run the suite first, every time.

Responding to review comments has two failure modes, and beginners hit both. Capitulating — changing the code because someone senior asked, without understanding why — leaves you unable to defend or maintain it, and quietly teaches you nothing. Getting defensive treats the comment as a judgement of you rather than of a diff. The middle path is a question: "I did it that way because X — does that hold, or am I missing something?" That sentence resolves most review disagreements in one round, because it surfaces the reasoning that the diff could not.

Reviewing someone else's PR has an order too. Read the description first, so you know what the change is trying to do. Then read the tests, because they tell you what the author believed "working" means — and a change with no test is itself a comment worth making. Only then read the diff, and read it against the intent you now hold.

The distinction that makes you a good reviewer: separate "this is wrong" from "I would have done it differently". Wrong means it breaks, leaks, races, or lies — block on that, clearly, with the failing case. Different means it offends your taste, and taste is not a merge blocker. Say it as "non-blocking: I'd probably have..." and approve anyway. Reviewers who block on taste get routed around, and then nobody reviews their team's code at all.
Ask questions, do not issue instructions. "Change this to a set" assumes you understand constraints the author may know better than you. "Is there a reason this is a list rather than a set? I think membership checks are hot here" gets you the same fix when you are right, and saves you being wrong in writing when you are not. Review comments are permanent and public; phrase them as though the author is competent, because usually they are.

Capstone checkpoint

What
A thirty-minute self-assessed walkthrough of everything this track built, run as though someone were watching.
Where
Your machine, from a clean clone, with a screen recorder running if you can manage one.
When
Once, after the trace and the PR cycle are done. This is the end of the backend track.
How
Six items, in order, out loud. Then watch it back and note where you hesitated.

On a real team this would be a live walkthrough with a mentor, and the value would come from having to say things out loud to a person who can ask "why?". You do not have that person, so the substitute is a recording — and it is a better substitute than it sounds, because hesitation is audible. The place where you go quiet, or say "and then it sort of... handles it", is exactly the part you do not understand. You will not notice that while talking. You will notice it immediately on playback.

  1. Clean clone, own README, under ten minutes. Clone your repository into an empty folder and bring the service up following only your own README, typing nothing you had to remember. If you reach for knowledge that is not in the file, stop and add it to the README — that gap is the finding.
  2. Live demo, authenticated. Register a user, log in, create a place, tag it, save it to a list, and fetch it back — using the token. All of it with curl or the interactive docs, narrating as you go.
  3. Explain one request end to end, without looking at the code. GET /places/1, from socket to JSON. This is the single item that matters most, and it is the one interviewers actually ask.
  4. Show something you broke on purpose and read the error. Stop the database mid-demo, or send an expired token, or post an invalid rating. Show the failure, then say what the error told you and how you would find it in logs.
  5. Read out your three unanswered questions from notes/request-trace.md, and say what you would do to answer each one.
  6. Watch the recording. Write down every point where you hesitated. Those are your next three study sessions, and they are better targeted than any curriculum could be.

What is not being assessed: elegance, how much of the traced codebase you read, whether your schema diagram is neat, how many endpoints exist, test coverage percentage, or whether your folder structure matches anyone's blog post. None of that is on the list, deliberately.

What is being assessed: whether it runs from a clean clone, and whether you can explain why it runs. That is the entire bar, and it is the same bar a first interview applies.

The README test is the one most people fail, and they fail it in the same way: the README says "run docker compose up" and omits that you must first copy .env.example to .env, because on your machine that file has existed since Module 00. A clean clone has no .env. Everything your README assumes but does not state is a thing that works only on your laptop — which is precisely the failure mode Module 11 spent seven hours eliminating from your container. Do not let it back in through the documentation.

What comes next map, not lesson

What
The named directions out of this track — the frontend half, and the backend topics deliberately left out.
Where
The track index lists the frontend modules; the backend gaps are yours to fill as jobs demand them.
When
After the capstone, not before. Every item below assumes the twelve modules behind you.
How
Pick one, and extend the City Guide API rather than starting something new.

The frontend half of this track follows the same shape: HTML, CSS and the DOM as three separate ideas; the browser runtime and its event loop; components and state; then the module that joins the halves — your own page calling your own API, which means CORS, token storage, and the security questions that come with putting a JWT anywhere a browser can reach. Then forms validated on both sides (client for speed, server for truth — never only one), and a build and deploy step.

What this backend track deliberately did not cover, named so you know they exist and know you have not learned them. Do not go and study these now; go and hit a problem that needs one.

TopicThe problem it solves
Background jobs and queuesWork too slow to do inside a request — emails, imports, image processing
CachingThe same expensive answer computed a thousand times a second
WebSocketsThe server needing to speak first, rather than only answering
Rate limitingOne client, deliberately or accidentally, consuming the service
CIRunning your tests automatically on every push, so nobody has to remember

Each of those is a module's worth of work and each one attaches to the project you already have. That is the last piece of advice this track has: keep extending the City Guide API. A project you have run for months teaches you things a fresh tutorial cannot, because only an old project has history, forgotten decisions, and code you have to trace — which is where this module started.

Build this

Target: a completed orientation checklist on a real repository, one full request trace written to notes/request-trace.md with line numbers and three unanswered questions, a branch-and-pull-request cycle practised end to end (against a local bare repo if you have no remote), and the capstone checkpoint completed — recorded, watched back, and its hesitations written down.
  1. Pick a repository — your own City Guide API, or any open-source Python web project you have or clone. Run all six orientation commands and write the answers down before opening any source file.
  2. Trace one concrete request end to end. Write notes/request-trace.md with the endpoint, the line-numbered path, the transformations, and three questions you cannot answer.
  3. Break it on purpose: in the traced codebase, change one line you are confident is unimportant — a default value, a constant, an ordering — then run its test suite and find out whether you were right. This is the fastest way to discover which parts of an unfamiliar system are load-bearing. Revert afterwards with git checkout -- <file>.
  4. Create a branch, commit the trace, push it to a remote (or to a local --bare clone), then merge it back and delete the branch.
  5. Run the README test: clone your own repo into an empty folder, follow only your README, and time it. Fix the README until a clean clone comes up in under ten minutes.
  6. Run the capstone checklist. Record it if you can. Write the hesitations into notes/module-12.md.

Verify it yourself

Every check below is objectively pass or fail from your own terminal. Run them in the repository you traced, then in your own.

$ git log --format=%an | sort | uniq -c | sort -rn | head -3
  312 Priya Raghavan                # counts, largest first, one line per author
   98 Tom Okafor                    # any names - the SHAPE is what you are checking
   41 dependabot[bot]

$ grep -rn "places" --include=*.py . | head -5
./app/routers/places.py:31:@router.get("/places/{place_id}")   # file:line:match
./app/main.py:26:app.include_router(places.router)             # non-empty = grep works

$ test -f notes/request-trace.md && echo FOUND || echo MISSING
FOUND                                # the trace file exists

$ grep -cE "\.py:[0-9]+" notes/request-trace.md
11                                   # must be 5 or more: line-numbered steps

$ grep -c "^[0-9]\." notes/request-trace.md
3                                    # the three numbered unanswered questions
                                     # (0 means you skipped the most valuable part)

# --- the README test: fresh clone, own README only, timed ---
$ mkdir -p /tmp/readme-test && cd /tmp/readme-test
$ date +%s > .start
$ git clone ~/projects/city-guide app
Cloning into 'app'...
done.                                # a local path clones fine - no network needed
$ cd app
# ... now follow ONLY your README, typing nothing from memory ...
$ echo $(( $(date +%s) - $(cat ../.start) )) seconds
412 seconds                          # under 600 = pass. Over = your README is wrong.

# --- the branch cycle ---
$ git remote -v
origin	/Users/you/projects/city-guide-origin.git (fetch)
origin	/Users/you/projects/city-guide-origin.git (push)

$ git branch --merged main
* main
  trace/places-notes                 # listed here = it was merged. Absent = it wasn't.

$ git log --oneline -1
a1b2c3d add request trace for GET /places/1
If grep -c "^[0-9]\." returns 0, the module is not finished. A trace with no questions means one of two things: you traced something you already fully understood, or you wrote plausible guesses instead of admitting gaps. Both are worth going back for. The questions are the deliverable; the path is just how you found them.

Prove it

Answer each in writing, in your own words, in notes/module-12.md, before opening the answers. This is the last set in the track — take it slowly.

1. Where do the structural choices of the codebase you traced differ from yours, and why might that be?

Show answer

Your answer is specific to what you traced, so judge it on whether you reached for reasons rather than verdicts. Real ones: age (a codebase from before FastAPI's dependency system will pass a session around by hand), scale (a repo with fifty endpoints splits crud/ out; one with four does not, and splitting early is over-engineering), team size (more authors means more layers, because layers are how people avoid editing the same file), and constraints you cannot see — a strange abstraction is often a scar from an incident. "This is worse than mine" is almost always a failure to find the constraint. Assume a reason exists and go looking for it in git log.

2. What are your three unanswered questions, and why is that list more valuable than the trace itself?

Show answer

The trace is reproducible — you can rebuild it in twenty minutes with grep. The questions are not, because they are a snapshot of the boundary of your knowledge, and that boundary moves and then you cannot remember where it was. Practically, the list is what makes you cheap to help: a senior can answer three file-and-line questions in five minutes, but cannot answer "I don't get it" at all. It is also the honest measure of progress — questions that get more specific over time mean you are learning, even on weeks where nothing you wrote worked.

3. What makes a pull request easy to review?

Show answer

One concern, small enough to hold in your head, a description saying what and why (the diff already says how), and passing tests before you ask. The underlying reason is that review is a scarce, expensive attention budget: a 900-line PR does not get 900 lines of scrutiny, it gets "LGTM" and false confidence, which is worse than no review because everyone now believes it was checked. Splitting a change into three PRs is not more work for the reviewer — it is less, because each one can be understood alone. The refactor and the fix should never be the same PR.

4. Why does reading a test suite tell you more than reading the README?

Show answer

Because a stale test fails and a stale README does not. A README is a claim written once, usually at the start, and nothing forces it to stay true. A test is executed on every run, so if the code drifts away from it, someone must either fix the code or change the test — either way the test tracks reality. Tests also record what a README never does: the edge cases, the error codes, the exact shapes. Read the test names first — forty names is a specification you can absorb in three minutes. And a behaviour with no test is a behaviour nobody has promised to keep.

5. Which single part of your own City Guide API would you now build differently, and what would you change?

Show answer

Any specific answer with a reason attached is a good one. Common honest ones: the schema, because a question arrived that the tables made awkward — which is exactly the Module 04 argument that queries drive the model. Error handling scattered across handlers instead of centralised. Auth checks written per-endpoint rather than as one dependency. Or tests that assert on internals and now break whenever you refactor. Note what the answer is not: "I'd use a different framework". Framework choice is rarely the thing that hurt you. What hurts is a decision made before you knew the question it was answering, which is unavoidable and is why the second version of anything is better.

6. You change one line you believe is unimportant and the test suite fails in three unrelated files. What have you learned?

Show answer

That the line was load-bearing and you could not see the load — which is the actual finding, not the failure. Three unrelated failures means hidden coupling: something reaches into that value from far away, probably through shared state, a default that everything relies on, or an implicit ordering. Read the three failing tests before fixing anything; together they describe the dependency the code does not document. This is also the most efficient probe you have in an unfamiliar system. Reading tells you what the code says; deliberately breaking it tells you what the code holds up, and those are different maps.