Code Comprehension Part 1: Hypotheses & Beacons
Part 1 of a two-part code comprehension tutorial for students who have completed the Python, React, and Node.js essentials tutorials. Practice deciding where to focus attention, reading faster with beacons, and verifying only the code that matters in a roughly one-hour session. Later quizzes interleave material from earlier steps — don't be surprised if a question in Step 6 asks about something you first met in Step 3.
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:
- Orient: read the file name, public function, test names, and comments that describe intent.
- Predict: say what responsibilities you expect to find.
- Beacon-hunt: scan for names, tests, data shapes, and familiar code plans.
- Descend selectively: inspect the smallest region that can answer your question.
- Verify: trace one path, map one test, or check one invariant.
- 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.
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))
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)
Step 1 — Knowledge Check
Min. score: 80%
1. Before tracing playlist_summary.py, which evidence gives the strongest first hypothesis about the module’s purpose?
A purpose hypothesis should start from specification-level beacons: test names, public function names, and behavior words. Here they point to summarizing a public playlist while excluding hidden tracks.
2. The question is: ‘Are hidden tracks included in the final summary?’ Which region should you inspect first?
visible_tracks() is the shortest path from the question to evidence. You only need to trace hidden=True through that filter and confirm summarize_playlist() uses the filtered list.
3. Which summary is strongest after reading this module?
A good comprehension summary climbs abstraction levels: user-visible purpose first, then the major behaviors that support it.
4. Which are reliable beacons in this small Python example? (select all that apply)
The strongest beacons are the ones that connect names to behavior: tests, helper names, and role-revealing variables.
5. Which is the strongest explanation of summarize_playlist() to share with a teammate?
Notice the shape of the strong answer: Purpose (what for), Mechanism (the plan), Evidence (what supports the claim), Limit (what you didn’t verify). That’s the EiPE frame — you’ll see it in every reading-notes.md from Step 2 onward. The Limit line is the one beginners skip and experienced reviewers always include.
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:
- Read function names and the sample data keys.
- Predict what
build_event_cards()returns. - Find the filtering step.
- Trace only one event through the relevant branch if needed.
- Vary the input mentally: change
todayortagand 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.
# 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:
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"))
Step 2 — Knowledge Check
Min. score: 80%
1. What is the best purpose hypothesis for build_event_cards()?
The helper names form a chain: upcoming events, tag matching, seats left, then card construction.
2. For today=14 and tag="frontend", which answer gives the result and the evidence?
React Snack Lab is upcoming, not cancelled, and has the requested tag.
3. Which detail can you safely ignore when answering whether cancelled events appear?
Attention control includes deciding what not to read. Seat math does not determine whether cancelled events enter the result.
4. Which small trace best verifies the hypothesis that full events are still included?
Node Night Market is full and not cancelled. Tracing it through the relevant helpers tests exactly the hypothesis.
5. Which note best shows useful vary-the-input reasoning?
Varying one input at a time helps verify which branch or helper actually controls the behavior.
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.
: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;
}
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 />);
Step 3 — Knowledge Check
Min. score: 80%
1. Which beacon most strongly tells you that App owns state that can change over time?
useState is a React framework beacon: it marks component-owned memory and a setter that can trigger a re-render.
2. The expression snacks.filter(...).map(...) is mainly which kind of beacon?
The structure compresses many details into a plan: choose the relevant data, then transform each item into JSX.
3. A reader wants to know what happens when the Vote button is clicked. Which path should they inspect?
The interaction crosses a component boundary: child button → callback prop → parent state update.
4. Which beacons are strong enough to support a first-pass prediction? (select all that apply)
A useful beacon has a tight relationship to behavior. Presentation class names are usually weaker than state, props, and list-rendering patterns.
5. Which cue is the weakest evidence for how votes change?
Weak-beacon diagnosis keeps fast reading from becoming careless guessing.
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.
# 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:
: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;
}
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 />);
Step 4 — Knowledge Check
Min. score: 80%1. Which component owns the selected topic state?
App owns state and passes it down. The child component sends changes upward by calling the callback prop.
2. If the selected topic is react, which task appears?
visibleTasks keeps tasks where task.topic === topic, so the React task remains.
3. When does the Everything visible is done. status appear?
The condition combines two checks: at least one visible task must exist, and every one of them must be done. Both gates are required.
4. Which statements are good evidence-based summaries of this React code? (select all that apply)
A strong React summary names state ownership, callback direction, and derived rendering.
5. What plan is shared by the Python event-card sprint and this React study-queue sprint?
Transfer improves when you name the shared code plan across languages: select relevant data, then shape it for the consumer.
6. You opened App.jsx and read the components. What is the next routine move before you commit to an answer about the completion banner?
The routine: Orient → Predict → Beacon-hunt → Descend selectively → Verify → Summarize. Verify follows beacon-hunting; the goal is one focused trace through the branch your hypothesis depends on, not a rewrite.
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.
# 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
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()}
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")
Step 5 — Hypothesis Repair
Min. score: 80%
1. For the tied-scores input — [('Ana','python',90), ('Ben','python',90), ('Ana','react',80)] — what does top_student_per_topic(scores) return?
The familiar surface said ‘max-tracking,’ so most readers commit to first-wins. The one detail that breaks the hypothesis is the operator: score >= leaders[topic][1] means a tying score replaces the current leader, so the LAST student with the maximum wins, not the first. This is exactly the moment to slow down and trace one tie carefully.
2. Which specific expression makes Ben win the tie over Ana?
When Ana’s row runs, 'python' not in leaders is True and Ana is installed. When Ben’s row runs, 'python' in leaders is True, so the second clause must decide — and 90 >= 90 is True. Ben overwrites Ana. Changing >= to > would preserve Ana.
3. What was the recovery move that exposed the gap between the familiar plan and this implementation?
When a hypothesis fails, the recovery isn’t ‘read everything again.’ It’s ‘choose the input that should expose the difference between the hypothesized behavior and the actual behavior, and trace exactly that one input through the conditional.’ This is the bottom-up repair move from the integrated meta-model.
4. Which observations about the existing tests are accurate? (select all that apply)
A useful secondary lesson: tests can appear to pin a behavior while actually leaving it under-specified. The name test_ties_resolved_by_appearance_order is itself a specification beacon — but the assertion doesn’t honor the name. A reviewer who skims test names without reading their assertions gets the wrong picture.
5. Which single-character change to the function would make the standard ‘max-tracking first-wins’ plan true?
The operator is the single point of difference between this code and the standard first-wins max-tracking plan. The brain’s pattern-matching said ‘this is max-tracking’; the code’s behavior said ‘this is last-wins max-tracking.’ One character separates the two interpretations. That gap is exactly the kind of detail false-beacon discipline applies to plans, not just to names.
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.
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))
Step 6 — Knowledge Check
Min. score: 80%
1. What role does topic_minutes play in summarize_minutes()?
The dictionary key is the topic, and the value accumulates minutes for that topic.
2. Which variables are the strongest beacons for the best-so-far plan?
A best-so-far plan usually keeps a current candidate and the score that justifies it.
3. What is the best purpose-level summary of recommend_badges()?
The purpose summary names the whole behavior and the major plans: aggregate, threshold-filter, and best-so-far.
4. Which trace best verifies that zero-minute entries are ignored?
A targeted trace follows exactly the branch that can confirm or refute the role hypothesis.
5. Which statements correctly connect variable roles to programming plans? (select all that apply)
Variable roles let readers name the plan before they trace all values.
6. The expression topic_minutes[topic] = topic_minutes.get(topic, 0) + entry["minutes"] is mainly which kind of beacon?
Beacon classes (Step 3): lexical (names), structural (code shapes), framework (library/API), specification (tests). The dict.get(key, default) + value pattern is the textbook aggregation plan — a structural beacon.
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.
# 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:
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"))
Step 7 — Knowledge Check
Min. score: 80%
1. What is the strongest purpose hypothesis for build_habit_report()?
The public function name and return keys point to a report-building purpose.
2. Which helper is central for deciding whether skipped check-ins count?
useful_checkins() contains the skipped/minutes predicate, so it is the shortest path for the filtering question.
3. If skill="python", which check-ins can affect total_minutes?
Vary-the-input reasoning follows the filters in order: useful first, skill second, totals third.
4. Which pair best shows the same aggregation purpose through different structures?
A clean contrast holds the purpose-level plan steady: many values become summary data. The surface structure can vary between a loop, dictionary update, or built-in reduction.
5. Which detail can you ignore if the only question is whether skipped check-ins contribute to totals?
Attention control means ignoring real code when it is not relevant to the current hypothesis.
6. Which plan labels fit this Python example? (select all that apply)
Interleaved plan recognition helps transfer: the same code can contain filtering, aggregation, and best-so-far tracking.
7. In longest_streak(), what role does previous_day play?
Variable roles (Step 5) compress code into plans. previous_day is a most-recent holder: keep the last value so you can ask ‘is this consecutive?’ on the next iteration. Recognizing the role makes the streak plan obvious without tracing every increment.
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 Mixernot 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.
# 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:
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))
Step 8 — Knowledge Check
Min. score: 80%
1. For audience="students" on day 9, why does Faculty Mixer not appear in the feed?
The smallest trace is build_feed() → is_active() → audience_can_see(). The pinned beacon is tempting, but it sits later in sorting and labeling.
2. Which line of reasoning best tests the pinned-announcement hypothesis?
Hypothesis testing means locating the cue in the call order. Here pinned is used after eligible is built.
3. What does the announcement contrast set reveal about pinned?
The contrast separates the surface cue from the critical feature: where the cue sits in the pipeline.
4. Which code regions are central evidence for the Faculty Mixer visibility question?
(select all that apply)
The central evidence is the eligibility path. pinned and channel are nearby cues, but neither decides this missing-item behavior.
5. Which final explanation best fits the evidence?
A strong false-beacon explanation says both parts: why the beacon was plausible and why the trace corrected it.
6. Your final explanation should follow the EiPE frame. What does the trailing L stand for?
EiPE = Purpose / Mechanism / Evidence / Limit. The Limit line is what separates honest comprehension from overclaiming. After a false-beacon trace especially, naming what you did not verify keeps the next reader honest.
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:
- Where is the borrow-eligibility decision implemented?
- Why did Maya’s loan succeed even though her fine exceeds the documented limit?
- 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.mdbefore 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.
# 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:
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
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},
]
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},
]
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},
]
"""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}
"""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)
"""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
"""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 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)
"""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")
Step 9 — Reading Boss Audit
Min. score: 80%1. Given the time budget, which reading strategy best fits this incident?
The routine for code that exceeds your reading budget: contract from the tests → the helper the contract implicates → one branch to confirm. Three artifacts instead of ten.
2. Where is the borrow-eligibility decision actually implemented?
The file name policy_service.py is itself an architectural beacon. The decision lives in can_borrow(), called from the lending orchestrator.
3. Why did Maya’s loan succeed even though her $15 fine exceeds the handbook’s $10 limit?
The code says $20, the handbook says $10. $15 sits in the gap. A classic spec-vs-implementation drift — and a reminder that comments and constants are weaker beacons than tests for actual behavior.
4. Classify the files you encountered. Which are CENTRAL evidence for this incident? (select all that apply)
Central = owns the decision or pins the spec. Supporting = supplies necessary data. Incidental = nearby but never on the runtime path. Concept location in a real codebase is mostly about what to skip.
5. In lending_service.lend_book(), what role does the local decision variable play?
Variable roles, revisited from Step 5: a single-value holder of an intermediate result. Recognizing the role compresses three lines into one mental chunk.
6. Why are the existing tests in test_lending.py insufficient to catch this bug?
Boundary tests are the natural place to catch threshold drift. The current tests probe well below and well above the contested boundary but never inside it. A regression test at unpaid_fine = 11.0 would have caught this immediately.
7. Which final EiPE-style explanation best fits the evidence?
A strong final summary names the spec drift, points at the exact constant, and is honest about what was not verified. EiPE is not bragging — the Limit line is where good reviewers earn trust.