A Field Guide

Reading Code

A method for approaching unfamiliar code — how to orient yourself, what to look for, how to trace execution, and how to evaluate what AI generates.

🗺️ Part 1: Mental Model
🧭 Part 2–3: Orienting
🔍 Part 4–5: Reading & Tracing
👥 Part 6: Other People's Code
🔁 Part 7: Patterns
🤖 Part 8: AI Code
🏗️ Part 9–10: Systems

Who This Is For

People who can write code but struggle when they encounter code they didn't write. People who feel confident building things from scratch but lost when dropped into an existing codebase. People who use AI to generate code and aren't sure how to evaluate what came back.

No particular language is assumed. The concepts here apply to Python, JavaScript, TypeScript, Ruby, Go, and most other languages you're likely to encounter.

This is a 35-minute read. By the end, you'll have a method for approaching unfamiliar code, know what to look for and what to skip, understand how to trace execution and follow data through a system, and have specific techniques for evaluating AI-generated code.

Why Reading Code Is a Distinct Skill

Most programming education is about writing code. Very little is about reading it. But in practice, most of the time you spend with code isn't writing new things — it's reading existing things.

Reading code you wrote last month. Reading code a colleague wrote. Reading a library to understand why it's behaving unexpectedly. Reading what an AI tool generated before you decide whether to use it. Debugging by tracing through unfamiliar logic.

The good news is that reading code is a learnable skill with real techniques. It's not just "stare at it until it makes sense." There are specific approaches — ways of orienting yourself, things to look for first, methods for tracing through logic — that consistently work.

Part One
Code as a Map
Why reading code isn't like reading prose — and the levels of understanding you actually need.

Reading Code Is Not Like Reading Prose

When you read a novel, you start at page one and proceed. Code is more like a map. A map has structure — regions, landmarks, roads — but you don't read it from top-left to bottom-right. You orient yourself first: where am I? What's around me? Where do I need to go?

Reading code works the same way. Before reading a single line, orient yourself. What is this thing? What does it do? How does it fit into the larger system? Then find the part that matters for what you're trying to understand.

💡 The Key Rule
Linear reading — starting at line 1 and reading to the end — is almost never the right approach for a file you didn't write.

Levels of Understanding

There are different levels of understanding, and you rarely need all of them:

Structural understanding: What pieces exist? What are the major functions, classes, or modules? How do they relate? You can get this without reading most of the code.

Behavioral understanding: What does this do? What goes in, what comes out, what side effects occur? You can often get this from names, comments, and tests.

Implementation understanding: How does it work internally? What algorithm is being used? This is the deepest level and usually only needed when debugging or changing the logic.

📝 Most of the Time
You need structural and behavioral understanding. Full implementation understanding is expensive — it requires reading carefully, holding a lot in your head, and it evaporates when you come back later. Don't reach for it unless you need it.

The goal isn't to memorize, it's to navigate. You don't need to hold an entire codebase in your head. You need to know enough about the structure to find what you're looking for when you need it.

Part Two
Orient Before You Read
Resist the urge to dive in. Five things to look at before reading any code.

Five Steps Before Reading Code

1. Start with the File Structure

Before opening any files, look at the directory structure. What folders exist? What are they named?

FolderTypically Contains
src/ or lib/Main source code
tests/ or __tests__/Tests (valuable to read — see below)
utils/ or helpers/Shared utility functions
models/ or schemas/Data structure definitions
routes/ or handlers/Incoming request handlers
components/UI pieces (frontend)
services/Business logic by domain
config/Configuration

This tells you how the author thought about the system. Even without reading a line, you start to understand the shape.

2. Find the README

If there's a README.md, read it. It often explains what the project does, how to set it up, and sometimes how it's organized. Ten seconds here saves minutes of confusion.

3. Find the Entry Point

Every program starts somewhere. In a web server: index.js, main.py, app.py, server.js. In a frontend app: main.jsx or index.tsx. The entry point is your anchor — skim it and note what it sets up and calls.

4. Read the Tests

Test files are often the best documentation of what code is supposed to do. A well-written test says: "given this input, this function should return this output." Before reading an implementation, scan the test file for it.

💡 Tests as Documentation
You'll quickly understand: what are the inputs, what are the expected outputs, what edge cases does the author care about? This often tells you more — and faster — than reading the implementation.

5. Look at the Imports

Imports tell you what the file depends on. External libraries show tools used. Internal imports show how pieces fit together.

import stripe
from .models import User, Order
from .utils import send_email
from .config import settings

From this alone: this file interacts with Stripe, uses User and Order models, sends emails, and reads from settings. Before reading a single function.

Part Three
How Execution Flows
Code doesn't run top to bottom — it runs in the order it's called.

Programs Don't Run Top to Bottom

Except for the very simplest scripts, code doesn't run in the order it's written. It runs in the order it's called. A function defined at line 500 might be the first thing that actually executes.

This is why reading linearly doesn't work. You need to follow the execution path, not the file order.

The Call Stack

When code calls a function, execution jumps into that function. When the function returns, execution jumps back. If that function called another function, execution jumped there first, then came back.

This creates a stack: function A called function B called function C. Right now we're in C. When C returns, back to B. When B returns, back to A.

💡 Zoom Control
When you're in a function and it calls something, you can choose to follow the call (zoom in) or treat that call as a black box (stay zoomed out). You don't have to follow every call. Follow the ones that matter.

Entry Points in Different Contexts

ContextHow Execution Typically Flows
Web serverRequest → router → handler → business logic → database → response
Script / CLITop-level code or main() → process arguments → linear sequence
UI componentEvent-driven: code runs when something happens (click, load, mount)
Library / moduleNo single entry point. The public API (exports) is what matters.
Part Four
Reading a Function
Functions are the primary unit of code. Getting good at reading one is the core skill.

How to Read a Function

Start with the Signature

The function signature — name, parameters, and return type — tells you most of what you need to know.

def calculate_discount(user: User, order: Order, coupon_code: str) -> Decimal:

Before reading the body: this takes a user, an order, and a coupon code, and returns a Decimal. The name says what it does. The parameters say what it needs. The return type says what you get back.

Read for the Happy Path First

The happy path is what happens when everything works normally. Ignore error handling, edge cases, and exceptions on the first pass.

def get_user_orders(user_id: int) -> list[Order]:
    user = User.get(user_id)
    if not user:
        raise ValueError(f"User {user_id} not found")

    orders = Order.query.filter_by(user_id=user_id).all()
    return [order for order in orders if not order.cancelled]

Happy path: get the user, get their orders, return non-cancelled ones. That's what this function does. The error case (user not found) is secondary.

Then Note the Edge Cases

After the happy path, scan for: what can go wrong? What conditions trigger different behavior? In the example above: what if there are no orders? The filter returns an empty list. What if the database is down? The User.get() call would throw.

Don't Read Every Line

You don't need to understand every line unless you're debugging or changing logic. What you need is the behavioral contract (what goes in, what comes out), the main flow, and awareness of edge cases.

📝 Selective Reading
Selective reading is not laziness. It's efficient navigation. Skip implementation details of sorting, string formatting, and math until they become relevant.

Following Data

A powerful technique: pick a piece of data and trace its journey through the system. Where does it come from? What happens to it? Where does it end up?

The Data Journey

Raw HTTP request body
  → parsed as JSON
  → validated against schema
  → converted to domain object
  → processed (discount applied, inventory checked)
  → saved to database
  → converted to response format
  → returned as JSON

Following this chain for one piece of data (say, an order amount) through the system gives you a concrete thread to hold onto. You're not trying to understand everything — just what happens to this value.

Watch for Where It Can Be Modified

Mutable state — data that can be changed — is a common source of bugs. When following data, note: where is this value assigned? Can anything else modify it? If a function receives an object and modifies it in place, the caller sees those modifications too.

Where Does It Leave?

Data eventually goes somewhere: back to the user, into a database, to an external service, into a log. Understanding the destination often clarifies what transformations make sense along the way.

Part Six
Other People's Code
Names are documentation, comments explain why, and inconsistency signals problems.

Reading Code You Didn't Write

Names Are Documentation

Good variable and function names tell you what something is and does without reading the implementation. calculate_tax() is self-documenting. process() is not. user_order_total is clear. uot is not.

When reading unfamiliar code, start with names. They reveal intent.

Comments Explain Why, Code Explains What

Good comments explain why something is done a certain way. Bad comments restate what the code already says.

# BAD: restates the code
# Multiply price by 1.1 to add tax
total = price * 1.1

# GOOD: explains why
# Sales tax rate is hardcoded here; switch to dynamic lookup in #412
total = price * 1.1

When you see a comment explaining something non-obvious, pay attention. It's often pointing at a constraint, a bug that was fixed, or a design decision.

Comments Can Lie

Code is executed by the machine and must be correct to work. Comments are not executed. They can become outdated — the code changed and the comment wasn't updated.

⚠️ The Rule
When a comment and the code disagree, trust the code. The comment describes what someone intended; the code describes what actually happens.

Naming Inconsistencies Signal Problems

If a codebase uses user_id in some places and userId in others, or get_user and fetch_user and load_user interchangeably — these are signs of code that grew without consistent conventions. Be more careful in these areas.

Long Functions Are Harder

A function that does one thing and returns a clear result is easy to read. A 200-line function that does five things is hard. When reading a long function, mentally break it into sections. What is this block doing? What is that block doing? Name those sections in your head.

Part Seven
Patterns You'll See Everywhere
Recurring patterns that let you understand code without reading it word by word.

Common Code Patterns

Code is full of recurring patterns. Learning to recognize them means you can understand what code is doing at a glance — like recognizing a word without sounding out every letter.

Guard Clauses

Return early if preconditions aren't met. Gets edge cases out of the way so the happy path runs unindented and clear.

def process_order(order):
    if order is None:
        return None
    if order.cancelled:
        raise ValueError("Cannot process cancelled order")
    if len(order.items) == 0:
        return {"status": "empty"}

    # Happy path runs here, unindented and clear
    ...

Mapping and Filtering

Transforming or selecting from a collection. Universal across languages:

# Python
prices_with_tax = [price * 1.1 for price in prices]
active_users = [u for u in users if u.active]

// JavaScript
const pricesWithTax = prices.map(p => p * 1.1);
const activeUsers = users.filter(u => u.active);

Error Handling (Try/Catch)

try {
    const result = await fetch(url);
    const data = await result.json();
    return data;
} catch (error) {
    console.error("Fetch failed:", error);
    return null;
}

When reading: what is the happy path in the try block? What failure is handled in the catch? Is the error logged, re-thrown, swallowed silently, or converted to a return value?

⚠️ Silent Swallowing
Catching an error and returning nothing, with no logging, is often a bug. Look for catch blocks that don't do anything meaningful.

Async/Await

async function getUser(id) {
    const user = await db.users.findOne(id);
    return user;
}

await means "pause here until this resolves." async marks a function that contains awaits. Identify which operations are async (external calls, I/O) vs. regular synchronous logic.

Configuration Objects

createServer({
    port: 3000,
    host: 'localhost',
    debug: true,
    timeout: 5000
})

A single object with multiple optional properties, rather than many separate parameters. Look at the keys to understand what the function does.

Factory Functions

def create_user(name, email, plan='free'):
    user = User(name=name, email=email, plan=plan)
    user.send_welcome_email()
    user.initialize_default_settings()
    return user

Functions named create_X, build_X, make_X centralize the logic of creating something.

Part Eight
Reading AI-Generated Code
AI code is plausible, not verified. Your job is to bridge from plausible-general to correct-specific.

Evaluating AI-Generated Code

AI coding tools generate code quickly and it often looks correct. The risk isn't that AI code is obviously bad — it's that it can be subtly wrong in ways that aren't apparent without careful reading.

AI Code Is Plausible, Not Verified

AI generates code based on pattern-matching with training data. It produces code that looks like correct code for the described task — which is usually, but not always, the same as code that actually is correct.

A human writing code thinks about the specific system. AI produces a plausible general solution. Your job is to bridge from plausible-general to correct-specific.

Read It Like a Code Review

The right attitude isn't "let me check for obvious bugs." It's "let me understand what this does and verify it does what I intended."

Read every function. Understand every significant block. If you find yourself thinking "I'm not sure what this does but it looks right," that's a flag, not a green light.

Specific Things to Verify

☐

Does every function/method called actually exist? AI sometimes invents function names that sound right.

☐

Is the error handling right? AI often handles the happy path correctly and errors incorrectly or not at all.

☐

Are there hidden assumptions about types or values? AI might assume a value is always non-null or always a certain type.

☐

Does it match your conventions? Inconsistency creates maintenance problems and bugs.

☐

Is anything hardcoded that should be configurable? API endpoints, timeouts, magic numbers.

Read the Whole Thing Before Accepting

Especially for larger blocks, resist accepting the first function that looks right. Read the whole generated block. Often a later function has a problem that affects the first one.

⚠️ The Rule
Don't ship code you don't understand. If you can't explain what a function does and why it's doing it that way, you don't understand it well enough to be responsible for it.
Part Nine
When You Don't Understand
You will encounter code you don't understand. This is normal. Here's what to do.

Tools for When You're Stuck

Use Your Editor

Modern editors have features that make reading code much faster:

FeatureWhat It Does
Go to DefinitionClick a function name and jump to where it's defined
Find All ReferencesSee everywhere a function or variable is used
Hover for Type InfoIn typed languages, shows the type without navigating
Peek DefinitionSee the definition in an overlay without losing your place
💡 Learn the Shortcuts
These transform reading code from an archaeological dig into efficient navigation.

Read the Tests (Again)

If you don't understand what a function does, find the test for it. The test is a concrete example: this input → this expected output. Often faster to understand than the implementation.

Add a Print Statement and Run It

Sometimes the fastest way to understand code is to run it with a print statement at the confusing part. See what values are flowing through with real data. For a full treatment of print debugging, binary search, stack traces, and browser DevTools, see the Debugging field guide.

Ask AI to Explain It

Paste the confusing code and ask "what does this do and why is it written this way?" AI is genuinely good at explaining code — better, often, than it is at writing it.

Follow up with "what are the edge cases I should know about?" and "are there any non-obvious things to watch out for?"

Search for the Pattern

If you see a pattern you don't recognize, search for it by name. "Python decorator pattern" or "JavaScript generator function" will return conceptual explanations that help you understand every time you see the pattern again.

Part Ten
Building a System Mental Model
For larger codebases — how to build a map of what exists and how pieces fit together.

Understanding a Whole System

The Layered Approach

Don't try to understand everything at once. Build understanding in layers:

Layer 1: What does this system do? What problem does it solve? (README + ten minutes looking around)

Layer 2: What are the major components? What does each one do at a high level? (Half-hour of reading entry points and structure)

Layer 3: How do specific pieces work? Follow one request, one data flow, one feature end-to-end. (Deep understanding of one path)

Each layer adds detail. Go deep where you're actually working.

Draw It

When a system is complex, draw it. Boxes for major components, arrows for connections. The act of drawing forces you to clarify: how does A connect to B? You'll discover gaps in your understanding when you try to draw connections you don't fully understand.

Find the Seams

Every system has seams — boundaries where one piece hands off to another. The web layer and business logic. Business logic and the database. Your code and an external API.

💡 Where Bugs Live
Seams are where bugs often live (different pieces have different assumptions). Understanding what each piece receives and sends at these boundaries is often more important than understanding internal workings.

Revisit Your Model

Your mental model is always an approximation. As you work with the codebase, you'll find places where it was wrong. Update the model when you find gaps. Good engineers continuously refine their mental model. Bad ones keep acting on wrong assumptions.

Reading Code Cheat Sheet

Before Reading Any Code

☐

Look at the directory structure

☐

Find the README

☐

Find the entry point

☐

Look at the tests for behavioral documentation

☐

Scan the imports at the top of the file

Reading a Function

  1. Read the name and signature — what goes in, what comes out?
  2. Find the happy path — what does it do in the normal case?
  3. Note the edge cases — what conditions trigger different behavior?
  4. Skip implementation details unless you need them

Following Data

  1. Find where it enters the system
  2. Trace transformations — what happens along the way?
  3. Find where it exits or is stored
  4. Note where it can be modified (mutation)

When Stuck

☐

Use Go to Definition to follow calls

☐

Read the test for the confusing part

☐

Add a print statement and run it

☐

Ask AI to explain the specific code

☐

Search for the pattern name conceptually

Patterns to Recognize

PatternWhat It Looks Like
Guard clausesEarly returns at the top for edge cases
Map/filterTransforming or selecting from collections
Try/catchWrapping code that can fail
Async/awaitOperations that wait for external things
Factory functionscreate_X, build_X — functions that make things
Options objectsSingle config argument with named keys

AI-Generated Code Checklist

☐

Does every called function/method actually exist?

☐

Is error handling present and correct?

☐

Are there hidden type or value assumptions?

☐

Does it match the codebase's conventions?

☐

Are any values hardcoded that should be configurable?

☐

Can you explain every significant block?

What You Can Do Now

The fastest way to build reading skill is deliberate practice — actually reading code, not just writing it.

Pick a small open-source project in a language you use. Something that solves a problem you understand. Spend thirty minutes with it using the approach from this guide: orient first, find the entry point, read the tests, follow one request or one piece of data from entry to exit.

You don't need to understand everything. You need to understand enough to explain, at a high level, what the system does and how the main flow works.

Do this a few times with different projects and reading code becomes significantly less intimidating. You'll start to see the same patterns everywhere. The unfamiliar starts to feel like variations on familiar themes.

That's the shift. And it happens faster than most people expect.