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

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

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
    favorite: 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, "favorite": False, "hidden": False},
    {"title": "Null Pointer Mood", "artist": "The Exceptions", "seconds": 203, "favorite": True, "hidden": False},
    {"title": "Draft Demo", "artist": "Lab Notebook", "seconds": 92, "favorite": False, "hidden": True},
    {"title": "Cache Me Outside", "artist": "Miss Hit", "seconds": 228, "favorite": 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["favorite"]:
        label += " (favorite)"
    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 Track, summarize_playlist


def make_track(
    title: str,
    seconds: int,
    *,
    artist: str = "The Testers",
    favorite: bool = False,
    hidden: bool = False,
) -> Track:
    return {
        "title": title,
        "artist": artist,
        "seconds": seconds,
        "favorite": favorite,
        "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_favorite_visible_track_is_labeled() -> None:
    tracks = [
        make_track(
            "Null Pointer Mood",
            203,
            artist="The Exceptions",
            favorite=True,
        ),
    ]

    summary = summarize_playlist(tracks)

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


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 timer is feedback about your reading path, not a grade.

🎯 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.

Practice mode: on your first attempt, use the +5 min button if you are still collecting evidence. On a replay, try finishing inside the original 7 minutes. Before the quiz, write one reflection line about what you inspected first, what you skipped, and what evidence changed or confirmed your hypothesis.

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:

Timer reflection:
- First thing I inspected:
- Detail I deliberately skipped:
- Evidence that changed or confirmed my hypothesis:
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
:root {
  --page-bg: #f6f8fb;
  --panel-bg: #ffffff;
  --text: #172033;
  --muted: #526070;
  --border: #c7d2df;
  --accent: #00598c;
  --accent-strong: #00466d;
  --button-text: #ffffff;
  --focus: #ffbf47;
}

html.dark-mode {
  --page-bg: #101827;
  --panel-bg: #1d2736;
  --text: #f4f7fb;
  --muted: #c8d2de;
  --border: #516174;
  --accent: #7cc7ff;
  --accent-strong: #a7dcff;
  --button-text: #08111d;
  --focus: #ffd166;
}

body {
  margin: 0;
  background: var(--page-bg);
  color: var(--text);
  font: 16px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.snack-shell {
  max-width: 860px;
  margin: 0 auto;
  padding: 24px;
}

.snack-shell h1 {
  margin-block: 0 18px;
  font-size: 1.8em;
}

.snack-shell label {
  display: grid;
  gap: 6px;
  max-width: 280px;
  font-weight: 700;
}

.snack-shell select,
.snack-shell button {
  font: inherit;
}

.snack-shell select {
  min-height: 40px;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  background: var(--panel-bg);
  color: var(--text);
}

.snack-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
  margin-block-start: 20px;
}

.snack-card {
  display: grid;
  gap: 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  background: var(--panel-bg);
  box-shadow: 0 2px 8px rgb(23 32 51 / 0.08);
}

html.dark-mode .snack-card {
  box-shadow: 0 2px 10px rgb(0 0 0 / 0.35);
}

.snack-card h2,
.snack-card p {
  margin: 0;
}

.snack-card h2 {
  font-size: 1.2em;
}

.snack-card p {
  color: var(--muted);
}

.snack-card button {
  justify-self: start;
  min-height: 40px;
  min-width: 80px;
  border: 2px solid var(--accent);
  border-radius: 6px;
  padding: 8px 14px;
  background: var(--accent);
  color: var(--button-text);
  cursor: pointer;
}

.snack-card button:hover {
  border-color: var(--accent-strong);
  background: var(--accent-strong);
}

.snack-shell :focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}
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="snack-card">
      <h2>{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 treat the clock as feedback about your reading path.

🎯 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.

Practice mode: on your first attempt, use the +5 min button if you are still collecting evidence. On a replay, try finishing inside the original 8 minutes. Before the quiz, write one reflection line about what you inspected first, what you skipped, and what evidence changed or confirmed your hypothesis.

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:

Timer reflection:
- First thing I inspected:
- Detail I deliberately skipped:
- Evidence that changed or confirmed my hypothesis:
study-queue/styles.css
:root {
  --page-bg: #f7f9fc;
  --panel-bg: #ffffff;
  --text: #172033;
  --muted: #526070;
  --border: #c7d2df;
  --accent: #00598c;
  --accent-soft: #d9edf9;
  --done-bg: #dff4e7;
  --done-text: #14532d;
  --open-bg: #fff1c2;
  --open-text: #5c3d00;
  --focus: #ffbf47;
}

html.dark-mode {
  --page-bg: #101827;
  --panel-bg: #1d2736;
  --text: #f4f7fb;
  --muted: #c8d2de;
  --border: #516174;
  --accent: #7cc7ff;
  --accent-soft: #1f3850;
  --done-bg: #173a29;
  --done-text: #a8f0c6;
  --open-bg: #3f3211;
  --open-text: #ffe08a;
  --focus: #ffd166;
}

body {
  margin: 0;
  background: var(--page-bg);
  color: var(--text);
  font: 16px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.queue-shell {
  max-width: 860px;
  margin: 0 auto;
  padding: 24px;
}

.queue-shell h1 {
  margin-block: 0 18px;
  font-size: 1.8em;
}

.topic-label {
  display: grid;
  gap: 6px;
  max-width: 280px;
  font-weight: 700;
}

.topic-label select {
  min-height: 40px;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  background: var(--panel-bg);
  color: var(--text);
  font: inherit;
}

.completion-status {
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 10px 12px;
  background: var(--accent-soft);
  color: var(--text);
  font-weight: 700;
}

.task-list {
  display: grid;
  gap: 12px;
  margin-block-start: 18px;
}

.task-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  background: var(--panel-bg);
  box-shadow: 0 2px 8px rgb(23 32 51 / 0.08);
}

html.dark-mode .task-card {
  box-shadow: 0 2px 10px rgb(0 0 0 / 0.35);
}

.task-card h2,
.task-card p {
  margin: 0 0 8px;
}

.task-card h2 {
  font-size: 1.2em;
}

.task-card p {
  color: var(--muted);
}

.task-status {
  display: inline-block;
  border-radius: 999px;
  padding: 4px 10px;
  font-weight: 700;
}

.task-status.done {
  background: var(--done-bg);
  color: var(--done-text);
}

.task-status.open {
  background: var(--open-bg);
  color: var(--open-text);
}

.queue-shell :focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 3px;
}
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 className="topic-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>{task.title}</h2>
      <p>Topic: {task.topic}</p>
      {task.done ? (
        <strong className="task-status done">Done</strong>
      ) : (
        <span className="task-status open">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 className="completion-status" role="status">
          Everything visible is done.
        </p>
      )}
      <section className="task-list" 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

Hypothesis Repair — When the Familiar Plan Isn't Quite Right

Why this matters

Fast pattern recognition has a failure mode. The moment you see for X in collection: if condition: best = X, your brain shouts “max-tracking plan!” and you stop reading the details. Most of the time you’re right. But sometimes the surface looks familiar and the substance is subtly different — and rushing past the difference is where wrong answers live.

This step is engineered for one thing: to make you commit to a prediction, then discover that your prediction was almost right. The recovery move — slowing down on one chosen input, finding the specific detail that breaks the obvious hypothesis — is what experienced readers do when their first hypothesis fails. We’ll practice it on a real example.

🎯 You will learn to

  • Notice when a familiar plan has been subtly modified
  • Recover from a wrong prediction with a targeted trace, not a full re-read
  • Use one carefully chosen input to expose a gap your hypothesis missed

The Setup

top_student.py contains a function top_student_per_topic(scores). The docstring says:

“Returns a mapping of topic → top student for that topic. Input is a list of (student, topic, score) tuples.”

In prediction-notes.md, before reading the function body, write your prediction for this input:

scores = [
    ("Ana", "python", 90),
    ("Ben", "python", 90),   # ← Ana and Ben tie at 90
    ("Ana", "react", 80),
]

What does top_student_per_topic(scores) return? Write a specific dictionary literal in your notes, not a vague description. Then take the quiz.

The point isn’t getting the prediction right. The point is making a prediction before you read the code — so when you find out whether you were right, the surprise has somewhere to land.

Starter files
prediction-notes.md
# Hypothesis Repair Notes

## My prediction (before reading `top_student.py`)

For the tied-scores input shown in the instructions, I predict the function returns:

```
```

## The plan I think this is

## My recovery (after the quiz forces me to look)

The specific line / operator that didn't match my prediction:

The actual behavior is:

## One-line lesson for next time
top_student.py
def top_student_per_topic(scores):
    """Return a mapping of topic → top student for that topic.

    Input: list of (student, topic, score) tuples.
    """
    leaders = {}
    for student, topic, score in scores:
        if topic not in leaders or score >= leaders[topic][1]:
            leaders[topic] = (student, score)
    return {topic: name for topic, (name, _) in leaders.items()}
test_top_student.py
from top_student import top_student_per_topic


def test_single_topic_clear_winner() -> None:
    scores = [("Ana", "python", 90), ("Ben", "python", 70)]
    assert top_student_per_topic(scores) == {"python": "Ana"}


def test_multiple_topics_no_ties() -> None:
    scores = [
        ("Ana", "python", 90),
        ("Ben", "react", 85),
        ("Cam", "node", 75),
    ]
    assert top_student_per_topic(scores) == {
        "python": "Ana",
        "react": "Ben",
        "node": "Cam",
    }


def test_ties_resolved_by_appearance_order() -> None:
    scores = [
        ("Ana", "python", 90),
        ("Ben", "python", 90),
        ("Ana", "react", 80),
    ]
    # Which of Ana or Ben survives the tie?
    result = top_student_per_topic(scores)
    assert "python" in result
    assert result["python"] in ("Ana", "Ben")


if __name__ == "__main__":
    test_single_topic_clear_winner()
    test_multiple_topics_no_ties()
    test_ties_resolved_by_appearance_order()
    print("all tests passed")
6

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))
7

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 treat the timer as calibration.

🎯 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?

Contrast Set

Use this step to practice same purpose, different structure. The code has more than one way to express “collapse many values into a summary”:

Code shape Purpose-level plan Surface structure
minutes_by_skill() Aggregate useful minutes into summary data Explicit loop + dictionary accumulator
total_minutes = sum(totals.values()) Aggregate useful minutes into summary data Built-in reduction over values

The critical feature is the aggregation plan. The incidental feature is whether the aggregation is written as a loop, a dictionary update, or a built-in call.

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

Practice mode: on your first attempt, use the +5 min button if you are still collecting evidence. On a replay, try finishing inside the original 10 minutes. Before the quiz, write one reflection line about what you inspected first, what you skipped, and what evidence changed or confirmed your hypothesis.

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:

Contrast set:
- Same purpose:
- Different structures:
- Critical feature I should transfer:

Final EiPE summary:

Timer reflection:
- First thing I inspected:
- Detail I deliberately skipped:
- Evidence that changed or confirmed my hypothesis:
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"))
8

Campus Feed Sprint

Why this matters

Real code often contains a clue that is true but not enough to answer the question. That is harder than a silly distractor, because the clue really does mean something. The move is not “ignore names.” The move is “turn the name into a hypothesis, then verify where it sits in the flow.”

In this sprint, a pinned faculty announcement is missing from the student feed. Your job is to test what pinned does, where eligibility happens, and which evidence actually explains the report. The timer is there to reveal whether your first search path was efficient, not to punish a careful repair.

🎯 You will learn to

  • Test a plausible first clue before trusting it
  • Verify a hypothesis by tracing the order of filters, sorting, and formatting
  • Explain why a true code fact can still be irrelevant to a specific question

Reading Goal

You have 8 minutes. Read campus_feed.py and answer the quiz.

Focus on this bug report:

“Why does Faculty Mixer not appear in the student feed on day 9 even though it is pinned?”

Contrast Set

Before tracing, compare three announcements as a matched set:

Announcement Surface cue Visibility result for students on day 9
Faculty Mixer pinned: true Hidden because audience is faculty
Old Survey pinned: true Hidden because it is archived and inactive
Study Jam pinned: false Visible because it passes eligibility

Same surface cue, different purpose: pinned changes ordering and labels after eligibility. It does not grant visibility. The critical feature is the cue’s position in the pipeline, not whether the value looks important.

Before tracing, write your first hypothesis in reading-notes.md. Then test it by reading the smallest path from build_feed() to the helper that decides eligibility.

Practice mode: on your first attempt, use the +5 min button if you are still collecting evidence. On a replay, try finishing inside the original 8 minutes. Before the quiz, write one reflection line about what you inspected first, what you skipped, and what evidence changed or confirmed your hypothesis.

Starter files
reading-notes.md
# Campus Feed Sprint Notes

First hypothesis:

First clue I noticed:

Evidence that supports it:

Evidence that changes or strengthens it:

Smallest trace:

Contrast set:
- Same surface cue:
- Different outcomes:
- Critical feature that explains the difference:

Final EiPE summary:

Timer reflection:
- First thing I inspected:
- Detail I deliberately skipped:
- Evidence that changed or confirmed my hypothesis:
campus_feed.py
from typing import TypedDict


class Announcement(TypedDict):
    title: str
    audience: str
    start_day: int
    end_day: int
    pinned: bool
    archived: bool
    channel: str


class FeedCard(TypedDict):
    title: str
    label: str
    channel: str


ANNOUNCEMENTS: list[Announcement] = [
    {
        "title": "Study Jam",
        "audience": "students",
        "start_day": 8,
        "end_day": 10,
        "pinned": False,
        "archived": False,
        "channel": "events",
    },
    {
        "title": "Faculty Mixer",
        "audience": "faculty",
        "start_day": 9,
        "end_day": 9,
        "pinned": True,
        "archived": False,
        "channel": "events",
    },
    {
        "title": "App Maintenance",
        "audience": "all",
        "start_day": 9,
        "end_day": 9,
        "pinned": False,
        "archived": False,
        "channel": "alerts",
    },
    {
        "title": "Old Survey",
        "audience": "students",
        "start_day": 1,
        "end_day": 3,
        "pinned": True,
        "archived": True,
        "channel": "survey",
    },
]


def is_active(announcement: Announcement, day: int) -> bool:
    return (
        announcement["start_day"] <= day <= announcement["end_day"]
        and not announcement["archived"]
    )


def audience_can_see(announcement: Announcement, audience: str) -> bool:
    return announcement["audience"] in ("all", audience)


def pinned_label(announcement: Announcement) -> str:
    return "Pinned" if announcement["pinned"] else "Regular"


def card_for(announcement: Announcement) -> FeedCard:
    return {
        "title": announcement["title"],
        "label": pinned_label(announcement),
        "channel": announcement["channel"],
    }


def build_feed(
    announcements: list[Announcement],
    audience: str,
    day: int,
) -> list[FeedCard]:
    eligible = [
        announcement
        for announcement in announcements
        if is_active(announcement, day)
        and audience_can_see(announcement, audience)
    ]
    ordered = sorted(
        eligible,
        key=lambda announcement: (
            not announcement["pinned"],
            announcement["title"],
        ),
    )
    return [card_for(announcement) for announcement in ordered]


if __name__ == "__main__":
    print(build_feed(ANNOUNCEMENTS, audience="students", day=9))
9

Reading Boss: Library Lending Audit

Why this matters

Every earlier step was a warm-up. The files were small enough that you could have read top-to-bottom in 90 seconds and still answered the quiz. The routine was useful but not necessary.

This step is different on purpose. The codebase is around 300 lines across 10 files. You cannot read it all carefully during a first-attempt timer and still leave room to answer well. The routine you have been practicing is what makes the answer reachable.

This is the situation top-down reading exists for: not because tracing is bad, but because some codebases exceed what a careful linear read can fit. When that happens, hypothesis-driven reading, targeted tracing, and strategic skipping are the only honest strategies.

🎯 You will learn to

  • Apply the full reading routine to code too large to read exhaustively
  • Locate a concept (the borrow-eligibility decision) across many plausible files
  • Identify which files are central, supporting, or incidental — without reading every line
  • Combine every Part 1 skill: hypothesis, beacons, variable roles, false-beacon discipline, EiPE

The Incident

A library patron named Maya Chen filed a complaint. Her record shows a $15 unpaid fine, yet the system let her borrow a new book today. The patron handbook (the spec) says:

“Patrons with unpaid fines exceeding $10 must clear their balance before borrowing.”

But Maya borrowed at $15. The librarians want answers:

  1. Where is the borrow-eligibility decision implemented?
  2. Why did Maya’s loan succeed even though her fine exceeds the documented limit?
  3. Which files are central evidence vs. incidental nearby code?

Time Budget Reality Check

Your first attempt gives you 15 minutes so the clock supports learning instead of panic. Treat 10 minutes as a replay challenge after you already know what a good search path feels like.

Strategy Roughly how long it takes
Read every file carefully 25–30 minutes
Skim every file 12–15 minutes
Read tests + one helper + one policy file 5–7 minutes ← target

Only the third strategy leaves enough time to think, answer, and write evidence. That is exactly the point of the boss.

Constraints

  • Read at most 3 files carefully.
  • Verify any beacon you decide to trust by checking imports or call sites.
  • Write your EiPE summary in reading-notes.md before the timer runs out.
  • End by writing a timer reflection: what you inspected first, what you deliberately skipped, and what evidence changed or confirmed your hypothesis.

The routine is yours to choose. Let’s see how far you can make it.

Starter files
reading-notes.md
# Library Lending Audit Notes

## Routine
Orient → Predict → Beacon-hunt → Descend selectively → Verify → Summarize

## First hypothesis (where is the fine-check implemented?)

## Files I read carefully (3 or fewer expected)
1.
2.
3.

## Files I deliberately skipped (and why)

## Smallest trace that explains Maya's case

## Final EiPE summary
- Purpose:
- Mechanism:
- Evidence:
- Limit (what I did *not* verify):

## Timer reflection
- First thing I inspected:
- File or detail I deliberately skipped:
- Evidence that changed or confirmed my hypothesis:
models.py
from typing import TypedDict


class Book(TypedDict):
    isbn: str
    title: str
    category: str
    available: bool


class Patron(TypedDict):
    patron_id: str
    name: str
    plan: str
    unpaid_fine: float
    active: bool


class Loan(TypedDict):
    loan_id: str
    patron_id: str
    isbn: str
    day_out: int
    day_due: int
    returned: bool
books_data.py
from models import Book


BOOKS: list[Book] = [
    {"isbn": "978-001", "title": "Designing Data-Intensive Applications", "category": "technical", "available": True},
    {"isbn": "978-002", "title": "The Pragmatic Programmer", "category": "technical", "available": True},
    {"isbn": "978-003", "title": "Clean Code", "category": "technical", "available": False},
    {"isbn": "978-004", "title": "Code Reading", "category": "technical", "available": True},
    {"isbn": "978-005", "title": "Refactoring", "category": "technical", "available": True},
    {"isbn": "978-006", "title": "Working Effectively with Legacy Code", "category": "technical", "available": True},
    {"isbn": "978-007", "title": "Soul of a New Machine", "category": "narrative", "available": True},
]
patrons_data.py
from models import Patron


PATRONS: list[Patron] = [
    {"patron_id": "p100", "name": "Maya Chen", "plan": "student", "unpaid_fine": 15.0, "active": True},
    {"patron_id": "p101", "name": "Jordan Kim", "plan": "faculty", "unpaid_fine": 0.0, "active": True},
    {"patron_id": "p102", "name": "Alex Park", "plan": "alumni", "unpaid_fine": 25.0, "active": True},
    {"patron_id": "p103", "name": "Sam Reyes", "plan": "student", "unpaid_fine": 0.0, "active": False},
    {"patron_id": "p104", "name": "Robin Tan", "plan": "faculty", "unpaid_fine": 5.0, "active": True},
    {"patron_id": "p105", "name": "Devi Rao", "plan": "alumni", "unpaid_fine": 0.0, "active": True},
]
loans_data.py
from models import Loan


LOANS: list[Loan] = [
    {"loan_id": "l001", "patron_id": "p100", "isbn": "978-002", "day_out": 5, "day_due": 19, "returned": False},
    {"loan_id": "l002", "patron_id": "p101", "isbn": "978-003", "day_out": 1, "day_due": 15, "returned": True},
    {"loan_id": "l003", "patron_id": "p102", "isbn": "978-004", "day_out": 8, "day_due": 22, "returned": False},
    {"loan_id": "l004", "patron_id": "p104", "isbn": "978-001", "day_out": 12, "day_due": 26, "returned": False},
    {"loan_id": "l005", "patron_id": "p105", "isbn": "978-006", "day_out": 14, "day_due": 28, "returned": False},
]
lending_service.py
"""Orchestrates the lending workflow: lookup, policy check, loan creation."""

from books_data import BOOKS
from patrons_data import PATRONS
from loans_data import LOANS
from models import Book, Patron, Loan
from policy_service import can_borrow


def find_book(isbn: str) -> Book | None:
    return next((book for book in BOOKS if book["isbn"] == isbn), None)


def find_patron(patron_id: str) -> Patron | None:
    return next((patron for patron in PATRONS if patron["patron_id"] == patron_id), None)


def next_loan_id() -> str:
    return f"l{len(LOANS) + 1:03d}"


def lend_book(patron_id: str, isbn: str, today: int) -> dict:
    patron = find_patron(patron_id)
    if patron is None:
        return {"status": "error", "reason": "patron not found"}

    book = find_book(isbn)
    if book is None or not book["available"]:
        return {"status": "error", "reason": "book unavailable"}

    decision = can_borrow(patron)
    if not decision["allowed"]:
        return {"status": "denied", "reason": decision["reason"]}

    new_loan: Loan = {
        "loan_id": next_loan_id(),
        "patron_id": patron_id,
        "isbn": isbn,
        "day_out": today,
        "day_due": today + 14,
        "returned": False,
    }
    LOANS.append(new_loan)
    book["available"] = False
    return {"status": "ok", "loan": new_loan}


def return_book(loan_id: str) -> dict:
    loan = next((entry for entry in LOANS if entry["loan_id"] == loan_id), None)
    if loan is None or loan["returned"]:
        return {"status": "error", "reason": "loan not active"}
    loan["returned"] = True
    book = find_book(loan["isbn"])
    if book is not None:
        book["available"] = True
    return {"status": "ok", "loan_id": loan_id}
policy_service.py
"""Encodes loan policy. The borrow-eligibility decision lives here."""

from models import Patron


MAX_FINE_BEFORE_BLOCK = 20.0
MAX_ACTIVE_LOANS = {"student": 3, "faculty": 8, "alumni": 2}


def can_borrow(patron: Patron) -> dict:
    if not patron["active"]:
        return {"allowed": False, "reason": "patron account inactive"}

    if patron["unpaid_fine"] > MAX_FINE_BEFORE_BLOCK:
        return {"allowed": False, "reason": "unpaid fine exceeds limit"}

    return {"allowed": True, "reason": ""}


def borrow_limit(patron: Patron) -> int:
    return MAX_ACTIVE_LOANS.get(patron["plan"], 1)
fines_service.py
"""Calculates fines for overdue loans. Does NOT enforce borrow policy."""

from loans_data import LOANS
from patrons_data import PATRONS


DAILY_FINE = 0.50
GRACE_DAYS = 2


def overdue_days(loan: dict, today: int) -> int:
    if loan["returned"]:
        return 0
    return max(today - loan["day_due"] - GRACE_DAYS, 0)


def fine_for_loan(loan: dict, today: int) -> float:
    return overdue_days(loan, today) * DAILY_FINE


def total_fines_due_today(patron_id: str, today: int) -> float:
    patron_loans = [loan for loan in LOANS if loan["patron_id"] == patron_id]
    return sum(fine_for_loan(loan, today) for loan in patron_loans)


def add_fine_to_patron(patron_id: str, amount: float) -> None:
    patron = next((p for p in PATRONS if p["patron_id"] == patron_id), None)
    if patron is not None:
        patron["unpaid_fine"] += amount


def clear_fine_for_patron(patron_id: str) -> None:
    patron = next((p for p in PATRONS if p["patron_id"] == patron_id), None)
    if patron is not None:
        patron["unpaid_fine"] = 0.0
reports_service.py
"""Librarian dashboard reports. Not part of the lending decision path."""

from books_data import BOOKS
from loans_data import LOANS
from patrons_data import PATRONS


def patrons_with_unpaid_fines() -> list[dict]:
    return [
        {"name": patron["name"], "fine": patron["unpaid_fine"]}
        for patron in PATRONS
        if patron["unpaid_fine"] > 0
    ]


def available_book_count() -> int:
    return sum(1 for book in BOOKS if book["available"])


def overdue_loan_summary(today: int) -> dict:
    overdue = [
        loan for loan in LOANS
        if not loan["returned"] and loan["day_due"] < today
    ]
    return {"count": len(overdue), "loan_ids": [loan["loan_id"] for loan in overdue]}


def loans_by_plan() -> dict:
    counts: dict[str, int] = {}
    for loan in LOANS:
        patron = next((p for p in PATRONS if p["patron_id"] == loan["patron_id"]), None)
        if patron is None:
            continue
        counts[patron["plan"]] = counts.get(patron["plan"], 0) + 1
    return counts
audit_service.py
"""Audit log helpers. Used by librarian admin tools, not by borrowing."""

from loans_data import LOANS


def loan_history_for_patron(patron_id: str) -> list[dict]:
    return [dict(loan) for loan in LOANS if loan["patron_id"] == patron_id]


def open_loan_ids() -> list[str]:
    return [loan["loan_id"] for loan in LOANS if not loan["returned"]]


def returned_loan_ids() -> list[str]:
    return [loan["loan_id"] for loan in LOANS if loan["returned"]]


def loan_count() -> int:
    return len(LOANS)
test_lending.py
"""Behavior tests. Read these first to learn the spec."""

from policy_service import can_borrow


def test_inactive_patron_blocked() -> None:
    patron = {"patron_id": "p999", "name": "X", "plan": "student", "unpaid_fine": 0.0, "active": False}
    decision = can_borrow(patron)
    assert decision["allowed"] is False
    assert "inactive" in decision["reason"]


def test_patron_with_large_fine_blocked() -> None:
    patron = {"patron_id": "p999", "name": "X", "plan": "student", "unpaid_fine": 25.0, "active": True}
    decision = can_borrow(patron)
    assert decision["allowed"] is False
    assert "fine" in decision["reason"]


def test_patron_with_no_fine_allowed() -> None:
    patron = {"patron_id": "p999", "name": "X", "plan": "student", "unpaid_fine": 0.0, "active": True}
    decision = can_borrow(patron)
    assert decision["allowed"] is True


if __name__ == "__main__":
    test_inactive_patron_blocked()
    test_patron_with_large_fine_blocked()
    test_patron_with_no_fine_allowed()
    print("all tests passed")