Index Ai Academy
Phase 11 · Professional practice · Working in a team

Reading unfamiliar code

Find your way around a big codebase fast, with tools that map it for you.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. A plan for a new codebase
  2. Tools that help
  3. Find the definition among the mentions
  4. Read with questions
  5. Maps from code
  6. Who calls whom
  7. Listing the functions in a module
  8. 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:

  1. 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.
  2. Find the entry points: main, the web routes, the CLI commands, the job handlers. Everything starts somewhere.
  3. 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.
  4. Read the tests. They're working examples of how each part is meant to be used, with the edge cases someone cared about.
  5. Search. Look for names, error messages and log lines with grep or rg (ripgrep). An error message from a bug report often leads straight to the code that printed it.
  6. Ask history. git log -p <file> and git blame show who changed a line, when, and in which pull request, which often explains why.
  7. Draw a map: the main modules and how they talk. Keep it rough.
Try it now

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.
Worked example

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)
It runs right here in your browser, with nothing to install.

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.

Quick check
A bug report says users see "Card declined: code 51". You've never seen this codebase. What's the fastest first step?
  1. Ask the user to try again
  2. Read every file from the top
  3. Search the code for the message text, such as "Card declined"
  4. Rewrite the payment module
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.

Worked example

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))
In the lesson you can change this and run it.

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.

Quick check
A static call graph says nothing calls export_report. Why might deleting it still break something?
  1. Unused functions make code faster
  2. Call graphs are always wrong
  3. Python needs every function defined
  4. It may be called dynamically: by getattr, a config file, a URL route or another repository
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.

Worked example

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)
In the lesson you can change this and run it.

Classes hold their methods in their own body. Walking a function's body finds the calls it makes.

Quick check
The top level of a parsed module. What does this print?
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])
  1. ['ClassDef', 'FunctionDef']
  2. ['ClassDef', 'FunctionDef', 'FunctionDef']
  3. ['FunctionDef', 'FunctionDef']
  4. ['Module']
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

Create a free account

Try a quiz question

Why read a project's tests early?

  1. They show how each part is meant to be used, including the edge cases someone cared about
  2. Tests are shorter than the code, so they're quicker to read through, and the rest of the code can wait
  3. Tests never change once they're written, so they show how the project was first designed to be used
  4. So you can delete them, since many old tests no longer run and only get in the way of new changes

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson