Nobody teaches you how to read code.
You are taught how to write it. Syntax. Data structures. Design patterns. How to name variables. How to structure a function. Entire curricula dedicated to the craft of producing code from nothing.
Then someone hands you a codebase and says: figure it out.
The Wrong Starting Point
The instinct, when dropped into an unfamiliar codebase, is to start at the top.
Open the main file. Read from line one. Try to understand everything before moving on. This feels thorough. It is actually the slowest possible approach.
A real codebase is not a textbook. It was not written to be read linearly. It was written incrementally, by multiple people, over months or years, each solving a different problem with slightly different assumptions. Reading it from the top is like trying to understand a city by reading every street sign in alphabetical order.
The file you open first is almost never where the interesting thing happens.
Find the Entry Point
Before reading any code, ask: what does this thing actually do?
Then find where that thing starts.
For a web server, that is the route handler. For a CLI tool, it is the argument parser. For a script, it is the function that gets called first. For a library, it is the public interface — the functions someone else actually calls.
Start there. Read outward from behavior, not inward from structure.
Entry points are usually small. They take input, call other things, return output. Following what they call tells you what the code actually cares about. Everything else is infrastructure.
grep Before You Read
When I land in an unfamiliar codebase, I reach for grep before I open any file to read.
grep -r "functionName" .
grep -rn "TODO\|FIXME\|HACK" .
grep -r "error\|panic\|fatal" . --include="*.go"
Searching for the thing I care about is faster than navigating to it. grep shows me where a function is defined, where it is called, and how many places depend on it. That context — who calls this and from where — tells me more about a function’s purpose than reading the function body does.
The -n flag gives line numbers. The -r flag searches recursively. These two flags together are the fastest way to orient yourself in any codebase.
git blame Tells You Why
Code tells you what. git blame tells you why.
git blame src/auth.py
git log --follow -p src/auth.py
git log --oneline --all
When I see something strange — an unusual pattern, a function that seems overcomplicated, a check that feels unnecessary — the first thing I do is blame the line. The commit message attached to it almost always explains the thing that confused me.
“fix: handle null case from API response” — and suddenly the extra null check makes sense.
“hotfix: revert caching after prod incident” — and suddenly the performance regression someone warned you about has context.
Reading code without reading its history is reading half the story. The code shows you the current state. The commits show you the journey.
Tests Are the Honest Documentation
Documentation lies. Not maliciously — it just drifts. Code changes, documentation does not always follow. After long enough, the README describes a system that no longer exists.
Tests do not lie. They run against the actual code. If they pass, they are true.
When I want to understand what a function does, I look at its tests before I look at its implementation.
test_login_with_valid_credentials → shows the happy path
test_login_with_expired_token → shows an edge case I need to know about
test_login_rate_limit_exceeded → shows a constraint I would have missed
Tests show you the intended behavior, the edge cases the author thought about, and the failure modes they cared about enough to test. That is more useful than reading the implementation cold.
If there are no tests, that is also information. It tells you to be careful.
Read by Behavior, Not by Line
The goal when reading unfamiliar code is not to understand every line. It is to understand what the code does and why.
These are different objectives and they require different approaches.
Understanding every line takes hours. Understanding behavior takes a question: if I call this with input X, what happens?
Trace that path. Skip what is not on the path. Mark the things you do not understand and come back to them once you have context. Most things that look confusing at first become obvious once you have seen how they fit.
The goal is a mental model, not a full transcript. You need to know enough to work safely — to make a change without breaking something you did not know existed. You do not need to have memorized the codebase.
The Skill Nobody Names
There is a specific skill involved in reading unfamiliar code, and it is not intelligence or experience, exactly.
It is patience with confusion.
When you write code, you always know what you mean. When you read someone else’s, you often do not — and that discomfort is real. The variable names that made sense to the author feel arbitrary to you. The structure that seemed obvious when it was built feels strange when you encounter it without context.
The instinct is to push through the confusion fast, to reach understanding quickly so you can start doing something. This instinct is wrong. Rushing past confusion just means you carry it forward.
The better approach is to sit with it. Mark the thing you do not understand. Keep reading. Let context accumulate. Most confusion resolves itself once you have seen enough of the surrounding code.
The developers who are fast at reading unfamiliar codebases are not the ones who read faster. They are the ones who are comfortable being lost for longer.
What Changes When You Get Good at This
Reading code becomes faster. Obviously.
But the less obvious thing is that writing code changes too.
When you have spent time reading other people’s code — their naming choices, their structure, their shortcuts — you develop a sharper sense of what makes code easy or hard to read. You start writing with a future reader in mind. You add the comment that would have saved you an hour. You name the variable after what it means, not what it is.
Every codebase you read is a lesson in what works and what does not. Some codebases are a lesson in what not to do. Those are often the most educational.
The first real codebase I had to navigate was not something I built. I spent the first two hours feeling like I was missing something obvious. I was not. The code was genuinely difficult to follow.
But eventually I found an entry point. I ran grep. I read a test. The fog started to thin.
That is how it always goes. Not a sudden understanding — a gradual one. And the only way through is to keep reading.