Reading unfamiliar code
Find your way around a big codebase fast, with tools that map it for you.
Create a free account Try part of it here
Why it matters
Professional engineers spend far more time reading code than writing it. On a new job, your first task is usually a bug in a codebase of 200,000 lines you didn't write. Nobody reads all of it. Experienced people use a plan and tools to find the 300 lines that matter, fast. That skill also makes you better at checking what AI tools write, and at writing code others can read.
What you'll be able to do
- Use a repeatable plan to find your way around a codebase you've never seen
- Follow one request or command from its entry point to the database and back
- Use search, tests, git history and AI assistants to answer questions about code
- Map a module with Python's ast: an outline, a call graph, and functions nothing calls
What it covers
- A plan for a new codebase
- Tools that help
- Find the definition among the mentions
- Read with questions
- Maps from code
- Who calls whom
- Listing the functions in a module
- You don't have to read it all
A plan for a new codebase
Nobody learns a new city by reading the whole map. You find the train station, walk one route, and learn more from there. Treat a new codebase the same way:
- Read the README and docs, then run it. Get it building, start it, run the tests. Seeing it work tells you more than an hour of reading.
- Find the entry points:
main, the web routes, the CLI commands, the job handlers. Everything starts somewhere. - Follow one path end to end. Pick one request, such as "add to cart", and trace it: route → handler → service → database → response. Write the path down.
- Read the tests. They're working examples of how each part is meant to be used, with the edge cases someone cared about.
- Search. Look for names, error messages and log lines with
greporrg(ripgrep). An error message from a bug report often leads straight to the code that printed it. - Ask history.
git log -p <file>andgit blameshow who changed a line, when, and in which pull request, which often explains why. - Draw a map: the main modules and how they talk. Keep it rough.
Nobody reads a codebase from start to finish. Follow one real path instead.
Tools that help
- Go to definition and find references in your editor, powered by a language server. They work like hyperlinks through the code.
- Call hierarchy: who calls this function, and what does it call?
- A debugger with a breakpoint on the path you're tracing: run the code and look at real values. It's like pausing a film to study one frame.
- AI assistants are good at "explain this module", "where is X handled?" or "what calls this?". Treat answers as leads, and check them in the code: they can sound sure and be wrong.
Find the definition among the mentions
Searching for a name finds every use. A more careful pattern finds where it's defined, which is usually where you want to start reading.
import re source = """ def total(items): return sum(items) class Cart: def total(self): return total(self.items) print(total([1, 2])) """ uses = [n for n, line in enumerate(source.splitlines()) if re.search(r"\btotal\b", line)] defs = [n for n, line in enumerate(source.splitlines()) if re.search(r"^\s*def total\b", line)] print("mentions on lines:", uses) print("definitions on lines:", defs)
Note there are two definitions: a function and a method with the same name. Knowing that early saves you from reading the wrong one. Editors do this properly by parsing the code, but a regex works in any repo, right away.
Read with questions
Nobody reads a phone book from front to back. You look up the name you need. Reading code without a goal is slow and forgettable too. Ask a question ("where do prices get rounded?"), find the answer, and move on. Keep a notes file of what you learn: it becomes the docs the next new person wishes they had.
In plain words: start with a question and read until it's answered, not from line 1 of file 1.
Show the answer
Search the code for the message text, such as "Card declined"
Error messages are exact strings, so searching for them lands you on the right line.
Maps from code
Python can read its own source as a tree with the ast module, a bit like diagramming a sentence into its parts. That makes custom maps easy: every function and class with its line, which functions call which, and which functions nothing calls. Real tools work the same way: linters, editors and code search all parse code into trees.
ast.walk(node) visits every node inside a tree. A call like total(items) is an ast.Call whose func is an ast.Name with id "total". A call like cart.total() has an ast.Attribute instead, which is harder to resolve without running the code. That's why static maps are a guide, not the full truth. A road map shows the roads, not where the traffic actually goes.
Who calls whom
Python can read its own source as a tree, so you can draw a map of any file. Here it finds which functions call which, and which ones nothing calls.
import ast source = """ def load(): return read() def read(): return 1 def save(): return 2 def main(): return load() """ calls = {} for node in ast.parse(source).body: if isinstance(node, ast.FunctionDef): calls[node.name] = [n.func.id for n in ast.walk(node) if isinstance(n, ast.Call)] print(calls) called = {name for targets in calls.values() for name in targets} print("nothing calls:", sorted(set(calls) - called))
main is the entry point, which is why nothing calls it. save is the interesting one: it might be dead code, or it might be called from another file. A map like this tells you where to ask questions.
export_report. Why might deleting it still break something?Show the answer
It may be called dynamically: by getattr, a config file, a URL route or another repository
Static maps only see direct calls by name. Search more widely, then delete with the tests running.
Listing the functions in a module
Each top-level statement is in tree.body.
import ast source = ''' def load(path): return parse(open(path).read()) def parse(text): return text.split(",") class Cart: def add(self, item): pass ''' tree = ast.parse(source) for node in tree.body: print(type(node).__name__, node.name, "line", node.lineno)
Classes hold their methods in their own body. Walking a function's body finds the calls it makes.
import ast tree = ast.parse("class A:\n def f(self): pass\n\ndef g(): pass\n") print([type(n).__name__ for n in tree.body])
Show the answer
['ClassDef', 'FunctionDef']
tree.body holds only top-level statements, a class and a function, so it prints ['ClassDef', 'FunctionDef']. The method f lives inside the class's own body.
You don't have to read it all
Trying to read a big codebase from start to finish is a trap. It's slow, and most of it won't stick, since you have no reason yet to care about any of it. Reading with a question works far better. Start from something real: a small bug, or a question such as "what happens when someone clicks Buy?". Then follow that one path. Your understanding grows outward from there, and you remember it because you used it.
Run it before you read it
You learn more from one test drive than from the manual. Running the code tells you so much that it's worth doing first, even if setup takes an hour. You learn what it does, how it starts, and whether the tests pass. If they fail before you've changed anything, that's essential to know, because otherwise you'll blame yourself later.
Check what AI tells you
AI tools are excellent at explaining unfamiliar code, and they're also confidently wrong at times. They may describe what a function's name suggests, not what the code does, or mix up two similar functions. Use them to get oriented quickly, then check the claims that matter. Open the file, or better, run the code and see.
"Unused" may not be unused
A search finds no callers, so the function looks dead. But it might be called by name from a config file, a scheduled job, a plugin system or another repository. Before you delete it, look at its history for why it was added. Then remove it in a small change of its own, and watch your logs after it ships. That way, it's easy to bring back if you were wrong. Deleting real dead code is valuable, and it's worth doing carefully.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Why read a project's tests early?