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.
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.
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.
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.
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.
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?
| Folder | Typically 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.
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.
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.
Entry Points in Different Contexts
| Context | How Execution Typically Flows |
|---|---|
| Web server | Request → router → handler → business logic → database → response |
| Script / CLI | Top-level code or main() → process arguments → linear sequence |
| UI component | Event-driven: code runs when something happens (click, load, mount) |
| Library / module | No single entry point. The public API (exports) is what matters. |
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.
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.
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.
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.
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?
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 userFunctions named create_X, build_X, make_X centralize the logic of creating something.
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.
Tools for When You're Stuck
Use Your Editor
Modern editors have features that make reading code much faster:
| Feature | What It Does |
|---|---|
| Go to Definition | Click a function name and jump to where it's defined |
| Find All References | See everywhere a function or variable is used |
| Hover for Type Info | In typed languages, shows the type without navigating |
| Peek Definition | See the definition in an overlay without losing your place |
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.
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.
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
- Read the name and signature — what goes in, what comes out?
- Find the happy path — what does it do in the normal case?
- Note the edge cases — what conditions trigger different behavior?
- Skip implementation details unless you need them
Following Data
- Find where it enters the system
- Trace transformations — what happens along the way?
- Find where it exits or is stored
- 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
| Pattern | What It Looks Like |
|---|---|
| Guard clauses | Early returns at the top for edge cases |
| Map/filter | Transforming or selecting from collections |
| Try/catch | Wrapping code that can fail |
| Async/await | Operations that wait for external things |
| Factory functions | create_X, build_X — functions that make things |
| Options objects | Single 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.