1

Purpose Hypotheses

Why this matters

When code feels overwhelming, the slowest move is often reading from the first line and hoping the purpose emerges. Strong readers start with a hypothesis: “This code probably does X, and these are the places that would prove or disprove it.” That is not guessing. It is evidence-seeking with a plan.

🎯 You will learn to

  • Generate a purpose hypothesis before tracing code line by line
  • Select which file or function deserves attention for a specific question
  • Summarize code at macro, function, and block levels

The Basic Routine

Use this loop whenever you meet unfamiliar code:

  1. Orient: read the file name, public function, test names, and comments that describe intent.
  2. Predict: say what responsibilities you expect to find.
  3. Beacon-hunt: scan for names, tests, data shapes, and familiar code plans.
  4. Descend selectively: inspect the smallest region that can answer your question.
  5. Verify: trace one path, map one test, or check one invariant.
  6. Summarize: explain the code above the line-by-line level.

When to Switch Gears

Top-down reading is not permission to trust names blindly. Use beacons to choose where to look, then switch to bottom-up tracing when the evidence gets risky.

Cue Good next move
A beacon is vague, like data or item Trace a small local chunk
A branch handles errors, hiding, or totals Trace one representative input
A test contradicts your first hypothesis Revise the hypothesis before reading more
Syntax is unfamiliar Understand that small expression first, then return to the purpose

Explain in Plain English

A strong answer is not the longest answer. Use this short EiPE-style frame:

Part What to say
Purpose What the code is for
Mechanism The main plan it uses
Evidence The names, tests, or branches that prove it
Limit What input, edge case, or behavior you have not verified yet

Line-by-line narration can be useful while tracing, but it is not the final product. Your final explanation should be relational: how the pieces work together to serve the purpose.

In this step, the Python files show a tiny playlist summarizer. Do not start by tracing every expression. Start from the tests and the public function name.

Worked Reading Example

Suppose the question is:

“Does the summary include hidden tracks?”

A bottom-up reader might trace every line. A hypothesis-driven reader does this:

Move What to read Why
Orient test_summary_uses_only_visible_tracks The test names the behavior in student-friendly language
Predict Expect a filtering step before summary construction Hidden tracks should be removed before labels and totals are computed
Beacon-hunt visible_tracks, hidden, summarize_playlist These names are evidence about responsibility
Descend Read visible_tracks() and where summarize_playlist() calls it That is the shortest path to the answer
Verify Trace one hidden track through visible_tracks() One path is enough to confirm the hypothesis

Variable Roles

Names become stronger beacons when you can say the role each value plays.

Variable Role
public_tracks Filtered collection used by the rest of the summary
total_seconds Accumulator result used to compute minutes
label One display string for one visible track

Your Turn

Read test_playlist_summary.py first, then scan playlist_summary.py. Write a one-sentence hypothesis:

“This module probably…”

Then answer the knowledge check. You do not need to edit the code.

Starter files
playlist_summary.py
from typing import TypedDict


class Track(TypedDict):
    title: str
    artist: str
    seconds: int
    explicit: bool
    hidden: bool


class PlaylistSummary(TypedDict):
    track_count: int
    minutes: float
    labels: list[str]


TRACKS: list[Track] = [
    {"title": "Compile Me Maybe", "artist": "Carly Rae JS", "seconds": 181, "explicit": False, "hidden": False},
    {"title": "Null Pointer Mood", "artist": "The Exceptions", "seconds": 203, "explicit": True, "hidden": False},
    {"title": "Draft Demo", "artist": "Lab Notebook", "seconds": 92, "explicit": False, "hidden": True},
    {"title": "Cache Me Outside", "artist": "Miss Hit", "seconds": 228, "explicit": False, "hidden": False},
]

def visible_tracks(tracks: list[Track]) -> list[Track]:
    return [track for track in tracks if not track["hidden"]]

def label_track(track: Track) -> str:
    label = f"{track['title']}{track['artist']}"
    if track["explicit"]:
        label += " (explicit)"
    return label

def summarize_playlist(tracks: list[Track]) -> PlaylistSummary:
    public_tracks = visible_tracks(tracks)
    total_seconds = sum(track["seconds"] for track in public_tracks)
    return {
        "track_count": len(public_tracks),
        "minutes": round(total_seconds / 60, 1),
        "labels": [label_track(track) for track in public_tracks],
    }

if __name__ == "__main__":
    print(summarize_playlist(TRACKS))
test_playlist_summary.py
import math

from playlist_summary import summarize_playlist


TrackFixture = dict[str, object]


def make_track(
    title: str,
    seconds: int,
    *,
    artist: str = "The Testers",
    explicit: bool = False,
    hidden: bool = False,
) -> TrackFixture:
    return {
        "title": title,
        "artist": artist,
        "seconds": seconds,
        "explicit": explicit,
        "hidden": hidden,
    }


def test_summary_uses_only_visible_tracks() -> None:
    tracks = [
        make_track("Compile Me Maybe", 181),
        make_track("Draft Demo", 92, hidden=True),
        make_track("Cache Me Outside", 228),
    ]

    summary = summarize_playlist(tracks)

    assert summary["track_count"] == 2
    assert summary["labels"] == [
        "Compile Me Maybe — The Testers",
        "Cache Me Outside — The Testers",
    ]


def test_explicit_visible_track_is_labeled() -> None:
    tracks = [
        make_track(
            "Null Pointer Mood",
            203,
            artist="The Exceptions",
            explicit=True,
        ),
    ]

    summary = summarize_playlist(tracks)

    assert summary["labels"] == [
        "Null Pointer Mood — The Exceptions (explicit)",
    ]


def test_minutes_total_counts_only_visible_seconds() -> None:
    tracks = [
        make_track("One Minute Opener", 60),
        make_track("Hidden Long Jam", 600, hidden=True),
        make_track("Two Minute Closer", 120),
    ]

    summary = summarize_playlist(tracks)

    assert math.isclose(summary["minutes"], 3.0)
2

Python Reading Sprint

Why this matters

Speed improves when you practice attention control, not when you rush. This timed round asks you to read just enough Python to answer targeted questions. The goal is not to finish perfectly; let’s see how far you can make it.

🎯 You will learn to

  • Apply hypothesis-driven reading under a realistic time box
  • Choose which details to ignore when the question is narrow
  • Verify a Python comprehension hypothesis with one focused trace

Reading Goal

You have 7 minutes. Read event_cards.py and answer the quiz. Use this plan:

  1. Read function names and the sample data keys.
  2. Predict what build_event_cards() returns.
  3. Find the filtering step.
  4. Trace only one event through the relevant branch if needed.
  5. Vary the input mentally: change today or tag and predict what behavior should change.

Use reading-notes.md to jot the evidence you find. The core target is the first three quiz questions. The full-event trace is a stretch target if you still have time.

Do not edit the code. This is a reading sprint, not an implementation task.

Starter files
reading-notes.md
# Python Reading Sprint Notes

Hypothesis:

Top 3 beacons:
1.
2.
3.

What I can ignore for this question:

Smallest trace:

Final EiPE summary:
event_cards.py
from typing import TypedDict


class Event(TypedDict):
    title: str
    day: int
    capacity: int
    registered: int
    cancelled: bool
    tags: list[str]


class EventCard(TypedDict):
    title: str
    spots: int
    status: str


EVENTS: list[Event] = [
    {"title": "Debugging Jam", "day": 14, "capacity": 30, "registered": 27, "cancelled": False, "tags": ["python", "tools"]},
    {"title": "React Snack Lab", "day": 17, "capacity": 24, "registered": 12, "cancelled": False, "tags": ["react", "frontend"]},
    {"title": "Legacy Code Karaoke", "day": 10, "capacity": 20, "registered": 20, "cancelled": False, "tags": ["reading"]},
    {"title": "Node Night Market", "day": 18, "capacity": 18, "registered": 18, "cancelled": False, "tags": ["node", "backend"]},
    {"title": "Cancelled CSS Cafe", "day": 20, "capacity": 12, "registered": 4, "cancelled": True, "tags": ["frontend"]},
]

def upcoming_events(events: list[Event], today: int) -> list[Event]:
    return [
        event
        for event in events
        if event["day"] >= today and not event["cancelled"]
    ]

def seats_left(event: Event) -> int:
    return max(event["capacity"] - event["registered"], 0)

def matches_tag(event: Event, tag: str) -> bool:
    return tag == "all" or tag in event["tags"]

def build_event_cards(
    events: list[Event],
    today: int,
    tag: str = "all",
) -> list[EventCard]:
    cards: list[EventCard] = []
    for event in upcoming_events(events, today):
        if matches_tag(event, tag):
            spots = seats_left(event)
            cards.append({
                "title": event["title"],
                "spots": spots,
                "status": "full" if spots == 0 else "open",
            })
    return cards

if __name__ == "__main__":
    print(build_event_cards(EVENTS, today=14, tag="frontend"))
3

Beacon Categories

Why this matters

Beacons are the cues that let you read code faster without becoming careless. In React, many beacons are framework-shaped: useState suggests local component memory, props suggest parent-to-child data flow, and .filter(...).map(...) often means “derive a visible list from data.” The skill is learning which cues deserve trust.

🎯 You will learn to

  • Classify lexical, structural, framework, and test/specification beacons
  • Use React beacons to predict component responsibilities
  • Decide when a beacon is too weak to trust without verification

Four Beacon Types

Beacon type What it looks like What it helps you infer
Lexical SnackCard, visibleSnacks, onVote Domain responsibility and data flow
Structural .filter(...).map(...), accumulator loops, guard clauses A familiar programming plan
Framework React.useState, props, onClick, key React-specific component behavior
Specification Tests, assertions, user stories, acceptance criteria Expected behavior before implementation

A beacon can be weak. A CSS class such as snack-grid is useful evidence about layout, but it is poor evidence for behavior such as persistence, filtering, or state ownership.

Predict Before Reading

Look at the React files. Before reading every JSX element, predict:

  • Which component owns the voting state?
  • Which component only displays one snack?
  • Which expression probably decides which snacks are visible?

Then inspect only the regions that can prove or disprove those predictions.

Starter files
snacks/styles.css
.snack-shell {
  max-width: 54rem;
  margin: 0 auto;
  padding: 1.5rem;
}

.snack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem;
}
snacks/App.jsx
const SNACKS = [
  { id: 1, name: "Mango Mochi", category: "sweet", votes: 9 },
  { id: 2, name: "Wasabi Peas", category: "savory", votes: 6 },
  { id: 3, name: "Chocolate Pretzels", category: "sweet", votes: 12 },
  { id: 4, name: "Seaweed Chips", category: "savory", votes: 4 },
];

function SnackCard({ snack, onVote }) {
  return (
    <article className="border rounded p-3">
      <h2 className="h5">{snack.name}</h2>
      <p>Category: {snack.category}</p>
      <p>Votes: {snack.votes}</p>
      <button type="button" onClick={() => onVote(snack.id)}>
        Vote
      </button>
    </article>
  );
}

function App() {
  const [category, setCategory] = React.useState("all");
  const [snacks, setSnacks] = React.useState(SNACKS);

  function handleVote(id) {
    setSnacks(currentSnacks => currentSnacks.map(snack =>
      snack.id === id ? { ...snack, votes: snack.votes + 1 } : snack
    ));
  }

  const visibleSnacks = snacks.filter(snack =>
    category === "all" || snack.category === category
  );

  return (
    <main className="snack-shell">
      <h1>Snack Vote</h1>
      <label>
        Category
        <select value={category} onChange={event => setCategory(event.target.value)}>
          <option value="all">All</option>
          <option value="sweet">Sweet</option>
          <option value="savory">Savory</option>
        </select>
      </label>
      <section className="snack-grid" aria-label="Snack choices">
        {visibleSnacks.map(snack => (
          <SnackCard key={snack.id} snack={snack} onVote={handleVote} />
        ))}
      </section>
    </main>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
4

React Reading Sprint

Why this matters

React code can look visually busy because JSX mixes data, structure, and event wiring. Beacon reading lets you find the small number of expressions that control the behavior you care about. This sprint is a little larger than the Python one, so use the routine and let’s see how far you can make it.

🎯 You will learn to

  • Apply React beacon reading to props, state, filters, and conditional rendering
  • Explain a component’s purpose without tracing every JSX tag
  • Decide which component owns a behavior

Reading Goal

You have 8 minutes. Your target is not “understand every line.” Your target is:

  • Who owns the selected topic?
  • Which tasks are visible?
  • When does the completion banner appear?
  • Which component is display-only?

Use reading-notes.md to record your hypothesis, strongest beacons, and smallest useful trace. As a transfer check, notice what this React code shares with the Python sprint: both derive a visible list, then shape that list for display.

After this step, wait two or three days before starting the advanced tutorial. That delay is not wasted time; it is how spacing turns today’s code-reading routine into something you can retrieve later.

Starter files
reading-notes.md
# React Reading Sprint Notes

Hypothesis:

Top 3 beacons:
1.
2.
3.

What I can ignore for this question:

Smallest trace:

Final EiPE summary:
study-queue/styles.css
.queue-shell {
  max-width: 58rem;
  margin: 0 auto;
  padding: 1.5rem;
}

.task-card {
  border: 1px solid #c9d3df;
  border-radius: 6px;
  padding: 1rem;
  margin-block: 0.75rem;
}
study-queue/App.jsx
const TASKS = [
  { id: 1, title: "Read route tests", topic: "node", done: true },
  { id: 2, title: "Trace prop callback", topic: "react", done: false },
  { id: 3, title: "Label accumulator role", topic: "python", done: true },
  { id: 4, title: "Map route to helper", topic: "node", done: false },
];

function TopicFilter({ topic, onTopicChange }) {
  return (
    <label>
      Topic
      <select value={topic} onChange={event => onTopicChange(event.target.value)}>
        <option value="all">All</option>
        <option value="python">Python</option>
        <option value="react">React</option>
        <option value="node">Node</option>
      </select>
    </label>
  );
}

function TaskCard({ task }) {
  return (
    <article className="task-card">
      <h2 className="h5">{task.title}</h2>
      <p>Topic: {task.topic}</p>
      {task.done ? <strong>Done</strong> : <span>Still practicing</span>}
    </article>
  );
}

function App() {
  const [topic, setTopic] = React.useState("all");

  const visibleTasks = TASKS.filter(task =>
    topic === "all" || task.topic === topic
  );

  const allVisibleDone = visibleTasks.length > 0 &&
    visibleTasks.every(task => task.done);

  return (
    <main className="queue-shell">
      <h1>Study Queue</h1>
      <TopicFilter topic={topic} onTopicChange={setTopic} />
      {allVisibleDone && <p role="status">Everything visible is done.</p>}
      <section aria-label="Visible study tasks">
        {visibleTasks.map(task => (
          <TaskCard key={task.id} task={task} />
        ))}
      </section>
    </main>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
5

Variable Roles and Plans

Why this matters

Beacons are easier to trust when you know the role each value plays. Novices often trace variable values forever; stronger readers ask whether a variable is a stepper, accumulator, candidate, flag, or organizer. That role label compresses several lines into a plan you can verify selectively.

🎯 You will learn to

  • Classify variables by role before tracing every update
  • Recognize aggregation and max-tracking plans from variable roles
  • Verify one role hypothesis with a small input trace

Variable Roles Are Beacons

Use this table while reading badge_recommender.py.

Role What it usually does Typical beacon
Stepper Moves through a collection entry, track, item inside a loop
Accumulator / gatherer Collects totals or output total, badges, topic_minutes
Candidate / best-so-far Remembers the current winner best_topic, best_minutes
Flag Records whether a condition has happened is_ready, has_warning
Organizer Groups data by key dictionary keyed by topic, id, or date

Worked Reading Example

If the question is:

“How does the code know which topic is strongest?”

Do not trace every entry first. Use the role beacons:

Move Evidence Hypothesis
Orient strongest_topic(topic_minutes) This helper probably chooses one topic
Beacon-hunt best_topic, best_minutes This looks like a best-so-far plan
Descend The if minutes > best_minutes branch The branch updates the current winner
Verify Trace {"python": 35, "react": 42} React should replace Python as best

Your Turn

Read the public function recommend_badges() first, then inspect the helpers. Write a role table in your notes:

  • Which value organizes minutes by topic?
  • Which values hold the current candidate?
  • Which value gathers the final user-facing output?
  • Which input path would you trace to verify that zero-minute entries are ignored?

You do not need to edit the code.

Starter files
badge_recommender.py
from typing import TypedDict


class ActivityEntry(TypedDict):
    topic: str
    minutes: int


TopicMinutes = dict[str, int]


ACTIVITY_LOG: list[ActivityEntry] = [
    {"topic": "python", "minutes": 18},
    {"topic": "react", "minutes": 12},
    {"topic": "python", "minutes": 22},
    {"topic": "node", "minutes": 35},
    {"topic": "react", "minutes": 0},
    {"topic": "python", "minutes": 15},
]

def summarize_minutes(entries: list[ActivityEntry]) -> TopicMinutes:
    topic_minutes: TopicMinutes = {}
    for entry in entries:
        if entry["minutes"] <= 0:
            continue
        topic = entry["topic"]
        topic_minutes[topic] = topic_minutes.get(topic, 0) + entry["minutes"]
    return topic_minutes

def strongest_topic(topic_minutes: TopicMinutes) -> tuple[str | None, int]:
    best_topic: str | None = None
    best_minutes = 0
    for topic, minutes in topic_minutes.items():
        if minutes > best_minutes:
            best_topic = topic
            best_minutes = minutes
    return best_topic, best_minutes

def recommend_badges(
    entries: list[ActivityEntry],
    minimum_minutes: int = 30,
) -> list[str]:
    topic_minutes = summarize_minutes(entries)
    topic, minutes = strongest_topic(topic_minutes)

    badges: list[str] = []
    for name, total in sorted(topic_minutes.items()):
        if total >= minimum_minutes:
            badges.append(f"{name}: {total} min")

    if topic and minutes >= minimum_minutes * 2:
        badges.append(f"focus: {topic}")

    return badges

if __name__ == "__main__":
    print(recommend_badges(ACTIVITY_LOG))
6

Python Plan Transfer Sprint

Why this matters

The final basic sprint asks you to transfer the same reading routine to a new Python example with a few more moving parts. This is where fluency starts to form: you see a new surface story, but the deeper plans are familiar. Use the beacons, keep your trace narrow, and let’s see how far you can make it.

🎯 You will learn to

  • Recognize familiar plans in a larger Python function
  • Separate central behavior from nearby helper detail
  • Use vary-the-input reasoning to test a comprehension hypothesis

Reading Goal

You have 10 minutes. Read habit_report.py and answer the quiz.

Focus on these questions:

  • What does build_habit_report() return?
  • Which helper removes skipped or zero-minute check-ins?
  • Which helper groups minutes by skill?
  • Which helper computes a streak?
  • What can you ignore if the question is only about filtering?

Use reading-notes.md to write a purpose hypothesis, three beacons, and one smallest useful trace. The stretch goal is the streak question.

Starter files
reading-notes.md
# Python Plan Transfer Sprint Notes

Purpose hypothesis:

Top 3 beacons:
1.
2.
3.

What I can ignore for this question:

Smallest trace:

Final EiPE summary:
habit_report.py
from typing import TypedDict


class Checkin(TypedDict):
    day: int
    skill: str
    minutes: int
    skipped: bool


class HabitReport(TypedDict):
    skills: list[str]
    total_minutes: int
    totals: dict[str, int]
    longest_streak: int


CHECKINS: list[Checkin] = [
    {"day": 1, "skill": "python", "minutes": 25, "skipped": False},
    {"day": 2, "skill": "python", "minutes": 20, "skipped": False},
    {"day": 3, "skill": "react", "minutes": 15, "skipped": False},
    {"day": 4, "skill": "python", "minutes": 0, "skipped": True},
    {"day": 5, "skill": "node", "minutes": 30, "skipped": False},
    {"day": 6, "skill": "python", "minutes": 18, "skipped": False},
]

def useful_checkins(checkins: list[Checkin]) -> list[Checkin]:
    return [
        checkin
        for checkin in checkins
        if not checkin["skipped"] and checkin["minutes"] > 0
    ]

def minutes_by_skill(checkins: list[Checkin]) -> dict[str, int]:
    totals: dict[str, int] = {}
    for checkin in checkins:
        skill = checkin["skill"]
        totals[skill] = totals.get(skill, 0) + checkin["minutes"]
    return totals

def longest_streak(checkins: list[Checkin], skill: str) -> int:
    days = sorted(
        checkin["day"]
        for checkin in checkins
        if checkin["skill"] == skill
    )
    longest = 0
    current = 0
    previous_day = None
    for day in days:
        if previous_day is None or day == previous_day + 1:
            current += 1
        else:
            current = 1
        longest = max(longest, current)
        previous_day = day
    return longest

def build_habit_report(
    checkins: list[Checkin],
    skill: str = "all",
) -> HabitReport:
    useful = useful_checkins(checkins)
    if skill != "all":
        useful = [checkin for checkin in useful if checkin["skill"] == skill]

    totals = minutes_by_skill(useful)
    total_minutes = sum(totals.values())
    streaks = {name: longest_streak(useful, name) for name in totals}

    return {
        "skills": sorted(totals),
        "total_minutes": total_minutes,
        "totals": totals,
        "longest_streak": max(streaks.values(), default=0),
    }

if __name__ == "__main__":
    print(build_habit_report(CHECKINS, skill="python"))