What you are learning — 4 topics
Getting your bearings
You do not read a codebase like a book; you pick one question and follow only the thread that answers it.
Reach for it whenTrace a single path when you need to change or fix one behaviour. Read broadly only when deciding architecture, which is rarer than it feels and usually premature.
Think of it likeArriving in an unfamiliar city. You do not memorise the street map — you learn the route from your hotel to the one place you need, then extend outwards from there.
Start from an entry point you can actually observe: a URL, a command, a button, a failing test. Anything that lets you watch the code react teaches more per minute than reading it does.
Read the data before the logic. Types, schemas and function signatures describe the shape of the system, and the logic is usually obvious once you know what flows through it.
Syntax you can userg -n "def target_name" .- Finds a function definition before reading a codebase at random.
rg -n "target_name" .- Finds every call site, test and reference for a name.
git log --oneline -10- Shows the recent changes that explain what the project has been doing.
pytest -q- Establishes whether the current checkout already passes its tests.
Several ways to see itWhat the machine doesLet the program show you instead of guessingA breakpoint or a print in the right place answers in seconds what an hour of reading leaves uncertain. Running the code is not a lesser form of understanding it — it is the fastest source of ground truth available to you.
How it breaksPlausible code is the dangerous kindCode that looks wrong gets read carefully. Code that reads smoothly gets approved. That is why subtle bugs survive review, and why generated code deserves more scrutiny than handwritten code rather than less.
In the wild- git bisect — finds the commit that introduced a bug without anyone reading the intervening code at all.
- Stack traces — hand you the exact call path into a failure, which is the fastest guided tour of an unfamiliar codebase you will get.
Commonly gets misread as- "I must understand the whole system before changing anything." Correction: nobody on the team understands the whole system either; they understand their paths through it and rely on tests for the rest.
- "Confusing code means I am not good enough yet." Correction: most confusing code is genuinely confusing, often written under deadline by someone who found it confusing too.
Go and read
Tracing one path end to end
Follow one real request from the moment it enters the system to the moment its result leaves, carrying the same concrete data all the way.
Reach for it whenTrace a path when one command, page or job behaves incorrectly. Build a broader architecture map only after several paths reveal which boundaries actually matter.
Think of it likeFollowing one parcel through a warehouse. Its scan history teaches you the receiving desk, conveyor, sorting rule and delivery gate without requiring you to study every parcel route.
Anchor the trace to an observable entry point and use a distinctive value you can search for, such as order 42 or a unique email address. Step into the handler, note each call that transforms or hands off the data, and mark boundaries: process, network, queue, database and filesystem. At each boundary record the value before and after, because that is where assumptions change shape.
Do not enter every helper. Read its signature and tests, then step inside only if it changes the value you care about or can explain the failure. Finish with a short path in your own words—route → validation → service → query → serializer—plus one observed value at each step. That path is useful; a folder-by-folder summary is usually not.
Syntax you can userg -n "/api/orders" app lib- Finds the route or handler that receives one request path.
print(f"{request_id=}, {user_id=}")- Labels one request's values while following its path.
breakpoint()- Pauses at a hand-off to inspect exactly what crosses the boundary.
raise ValueError(f"unexpected state: {state!r}")- Stops a trace at an impossible state with its useful value attached.
Several ways to see itIts shapeA path is a thin vertical sliceOne request cuts through routing, business logic, storage and presentation. Reading by directories shows horizontal layers but hides how they cooperate. The vertical slice gives each layer a purpose you can observe.
What the machine doesCarry one value across every handoffWatch order_id begin as text in a URL, become an integer after validation, enter a query and return as a field in JSON. A name alone can lie because different layers reuse it for different shapes. The concrete value reveals conversions, defaults and loss.
How it breaksBoundaries are where meanings driftA caller treats missing as null while the database treats it as an empty string; one service uses seconds while another uses milliseconds. Pause where ownership changes and compare both sides' contracts. Most cross-layer bugs are locally reasonable code joined by incompatible assumptions.
In the wild- OpenTelemetry — propagates a trace ID across services and records nested spans, letting one request be followed through HTTP calls, queues and database work.
- Django — resolves a URL to a view, passes an HttpRequest through middleware, and returns an HttpResponse through the same middleware stack.
- AWS X-Ray — joins service segments with a shared trace header so a slow or failing request can be located across distributed components.
Commonly gets misread as- "A call graph tells me the path." Correction: it lists what could call what; a trace shows what this request actually called with these values.
- "I should step into every function." Correction: descend only where relevant state changes or an unverified boundary is crossed, or the signal disappears under detail.
What it costsYou deliberately ignore most of the repository, trading completeness for a trustworthy explanation of the behavior you need now.
Go and read
Changing unfamiliar code safely
When your understanding is partial, make the smallest observable change and use tests to guard everything you are not holding in your head.
Reach for it whenUse this approach for fixes and small features in code you did not design. Stop and map more of the system when the change crosses ownership, persistence or security boundaries.
Think of it likeRepairing one pipe in a wall you cannot open completely. You shut off the water, photograph the old joints, replace one fitting and pressure-test before sealing anything.
Begin with a characterization test: feed the current code a representative input and record its observable output, even if the implementation looks odd. Add a failing test for the behavior you need, then make one focused diff that turns it green. Existing behavior outside that test is protected by the broader suite, so run the closest tests first and the full suite before handing off.
Avoid cleanup in the same change. Renames, dependency upgrades and abstraction rewrites expand the review surface and make a failure harder to attribute. If you discover necessary cleanup, separate it into a behavior-preserving commit with its own green test run. Small is not timid; small makes cause and effect visible.
Syntax you can usegit status --short- Shows local changes before you begin, so you do not mix work together.
git diff -- path/to/file- Reviews only the file you intended to change.
pytest -q- Checks that the existing behaviour still passes after a small edit.
git diff --check- Catches trailing whitespace and conflict-marker mistakes before sharing a diff.
Several ways to see itWhat it costsReview surface is a risk budgetEvery unrelated line asks the reviewer to hold another fact and creates another possible cause if CI fails. Spend that budget on the behavior that must change. Move cleanup to a separate diff so each result has one plausible cause.
Against the alternativeCharacterization comes before improvementA characterization test asks what the code does now; a specification test asks what it should do after the change. You often need both in unfamiliar code. The first stops accidental drift while the second proves the intended movement.
How it breaksHidden effects set the blast radiusA helper named save_user may also send mail, clear a cache and write an audit row. Search its callers and mocked dependencies before editing it. The number of changed lines says little if those lines sit at a high-fan-out boundary.
In the wild- GitHub protected branches — can require status checks and approving reviews before a small change reaches the default branch, turning safety evidence into a merge condition.
- CPython — runs focused tests with regrtest and then its buildbot fleet across operating systems, catching behavior a contributor's local machine cannot cover.
- Kubernetes — uses feature gates to ship behavior disabled or scoped first, allowing a risky change to be tested without immediately affecting every cluster.
Commonly gets misread as- "Small diffs are safe diffs." Correction: a one-line permission or schema change can have a huge blast radius; small makes review easier but does not replace tracing the effect.
- "The existing tests mean I do not need a new one." Correction: if no test fails before your implementation changes, you have not demonstrated that the suite covers the requested behavior.
What it costsMore test runs and occasionally an extra preparatory commit, exchanged for changes that are easy to review, revert and diagnose.
Go and read
Reading a change, not just a diff
A diff shows edited lines, not the full behavior they sit inside; review the contract and surrounding path before judging the patch.
Reach for it whenRead the diff first to locate the claimed change, then open every touched function and its callers when correctness depends on state, control flow or side effects outside the shown lines.
Think of it likeReviewing edits to three sentences cut from a contract. Red and green ink shows the wording change, but only the surrounding clauses tell you whether it creates a loophole.
First read the change description and state the contract in your own words. Scan the file list for surprising scope, generated files, migrations, permissions, dependency locks and tests. Then read tests before implementation when possible: they expose what the author believes changed, and omissions suggest the boundary cases to ask about.
A diff hides unchanged conditions above the hunk, callers that depend on the old result, moved code presented as delete-plus-add, and runtime behavior no line view can prove. Expand the full function, search every caller of a changed public symbol, and inspect the data before and after the edit. Finish by running the relevant path; review is an attempt to falsify the change, not a spelling check.
Syntax you can usegit diff- Shows your unstaged changes line by line.
git diff --stat- Summarises which files changed and by how much before the detail.
git diff --word-diff- Highlights the changed words inside a long line.
git show --stat HEAD- Summarises the latest commit before opening its full patch.
git diff --check- Finds whitespace errors and conflict markers in the diff.
Several ways to see itIts shapeThe file list is the first architecture clueA supposed label change touching authentication, schema and deployment files is telling you the real scope before you read a line. Group files by role and ask why each group moved. Surprising spread is either missing explanation or accidental coupling worth finding.
What the machine doesRead outward from every changed branchFor a modified condition, trace both true and false paths until their observable effects rejoin. For a changed return value, follow at least one caller. The diff marks where execution may diverge; the surrounding code shows what that divergence does.
How it breaksLook first where rollback would hurtSchema changes, destructive writes, permissions, money and external calls deserve attention before naming or formatting. Ask whether the operation is reversible, repeatable and safe after partial failure. Severity should set review order, not file order.
Against the alternativeTests explain intent; implementation reveals reachThe added test shows the case the author chose to prove. The implementation may affect a much larger input space. Compare the two and probe the gap, especially neighboring boundaries the test does not name.
In the wild- GitHub pull requests — combines a file diff with changed-file history, review comments and required checks, but reviewers must expand context or open the file to see untouched control flow.
- Gerrit — stores successive patch sets and supports comparing them, so reviewers can isolate what changed after feedback instead of rereading the whole proposal.
- git range-diff — compares two versions of a commit series, exposing how individual commits changed after a rebase or review rewrite.
Commonly gets misread as- "Green lines are the new behavior." Correction: an added condition may redirect unchanged code far below it, so the behavioral change can live outside every highlighted line.
- "A small diff deserves a quick review." Correction: a changed default, inequality or authorization check can alter every request while occupying one line.
What it costsOpening context and tracing callers takes longer than scanning colors, but it concentrates effort on behavior the colored lines cannot show.
Go and read
You are done with this stage when
Exit checkYou can clone a repository you have never opened, reproduce a reported bug, and explain the code path that causes it.
If you cannot do this, repeat the stage rather than moving on. Nothing later gets easier by skipping it.