{
const scripts = Array.from(
document.querySelectorAll("script.fcpython-ojs-quiz-config")
);
const script = scripts.find(
(node) => node.dataset.fcpythonRendered !== "true"
);
if (!script) {
return html`<div class="fcpython-quiz fcpython-quiz-warning">
Quiz configuration was not found.
</div>`;
}
script.dataset.fcpythonRendered = "true";
const quiz = JSON.parse(script.textContent);
const container = html`<div class="fcpython-quiz"></div>`;
const title = document.createElement("h3");
title.textContent = quiz.title;
container.appendChild(title);
const instructions = document.createElement("p");
instructions.textContent = quiz.instructions;
container.appendChild(instructions);
const progress = document.createElement("div");
progress.className = "fcpython-quiz-progress";
const counter = document.createElement("p");
counter.className = "fcpython-quiz-counter";
counter.setAttribute("aria-live", "polite");
progress.appendChild(counter);
const tabs = document.createElement("div");
tabs.className = "fcpython-quiz-steps";
tabs.setAttribute("role", "tablist");
tabs.setAttribute("aria-label", "Quiz questions");
progress.appendChild(tabs);
container.appendChild(progress);
const questions = document.createElement("div");
questions.className = "fcpython-quiz-questions";
const feedbackNodes = [];
const questionPanels = [];
const stepButtons = [];
let currentQuestion = 0;
function setStepStatus(questionIndex, status) {
const step = stepButtons[questionIndex];
const question = quiz.questions[questionIndex];
const statuses = {
answered: "answered",
correct: "correct",
incorrect: "incorrect",
unanswered: "not answered",
};
step.classList.remove(
"is-answered",
"is-correct",
"is-incorrect",
"is-unanswered"
);
if (status) {
step.classList.add(`is-${status}`);
}
const statusLabel = status ? `, ${statuses[status]}` : "";
step.setAttribute(
"aria-label",
`Question ${questionIndex + 1}: ${question.prompt}${statusLabel}`
);
}
function showQuestion(questionIndex, focusPanel = false) {
currentQuestion = Math.max(
0,
Math.min(questionIndex, quiz.questions.length - 1)
);
questionPanels.forEach((panel, index) => {
panel.hidden = index !== currentQuestion;
});
stepButtons.forEach((step, index) => {
const isCurrent = index === currentQuestion;
step.classList.toggle("is-current", isCurrent);
step.setAttribute("aria-selected", String(isCurrent));
step.tabIndex = isCurrent ? 0 : -1;
});
counter.textContent = `Question ${currentQuestion + 1} of ${quiz.questions.length}`;
const selected = questionPanels[currentQuestion].querySelector(
"input[type='radio']:checked"
);
const isLastQuestion = currentQuestion === quiz.questions.length - 1;
previous.hidden = currentQuestion === 0;
next.hidden = isLastQuestion || !selected;
check.hidden = !isLastQuestion;
if (focusPanel) {
questionPanels[currentQuestion].focus();
}
}
quiz.questions.forEach((question, questionIndex) => {
const tabId = `${quiz.id}-question-tab-${questionIndex + 1}`;
const panelId = `${quiz.id}-question-panel-${questionIndex + 1}`;
const step = document.createElement("button");
step.type = "button";
step.className = "fcpython-quiz-step";
step.id = tabId;
step.textContent = String(questionIndex + 1);
step.setAttribute("role", "tab");
step.setAttribute("aria-controls", panelId);
step.setAttribute("aria-selected", "false");
step.tabIndex = -1;
step.addEventListener("click", () => showQuestion(questionIndex));
step.addEventListener("keydown", (event) => {
let destination = null;
if (event.key === "ArrowRight") {
destination = (questionIndex + 1) % quiz.questions.length;
} else if (event.key === "ArrowLeft") {
destination =
(questionIndex - 1 + quiz.questions.length) % quiz.questions.length;
} else if (event.key === "Home") {
destination = 0;
} else if (event.key === "End") {
destination = quiz.questions.length - 1;
}
if (destination !== null) {
event.preventDefault();
showQuestion(destination);
stepButtons[destination].focus();
}
});
stepButtons.push(step);
tabs.appendChild(step);
setStepStatus(questionIndex, "");
const panel = document.createElement("div");
panel.className = "fcpython-quiz-panel";
panel.id = panelId;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tabId);
panel.tabIndex = -1;
const fieldset = document.createElement("fieldset");
fieldset.className = "fcpython-quiz-question";
const legend = document.createElement("legend");
legend.textContent = question.prompt;
fieldset.appendChild(legend);
question.options.forEach((option, optionIndex) => {
const label = document.createElement("label");
label.className = "fcpython-quiz-option";
const input = document.createElement("input");
input.type = "radio";
input.name = `${quiz.id}-${question.id}`;
input.value = String(optionIndex);
input.addEventListener("change", () => {
setStepStatus(questionIndex, "answered");
feedbackNodes[questionIndex].textContent = "";
feedbackNodes[questionIndex].className = "fcpython-quiz-feedback";
score.textContent = "";
showQuestion(questionIndex);
});
const text = document.createElement("span");
text.textContent = option;
label.appendChild(input);
label.appendChild(text);
fieldset.appendChild(label);
});
const feedback = document.createElement("p");
feedback.className = "fcpython-quiz-feedback";
feedback.setAttribute("aria-live", "polite");
feedbackNodes.push(feedback);
fieldset.appendChild(feedback);
panel.appendChild(fieldset);
questionPanels.push(panel);
questions.appendChild(panel);
});
container.appendChild(questions);
const actions = document.createElement("div");
actions.className = "fcpython-quiz-actions";
const previous = document.createElement("button");
previous.type = "button";
previous.className = "fcpython-quiz-secondary";
previous.textContent = "Previous";
previous.addEventListener("click", () => {
showQuestion(currentQuestion - 1, true);
});
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next question";
next.addEventListener("click", () => {
showQuestion(currentQuestion + 1, true);
});
const check = document.createElement("button");
check.type = "button";
check.textContent = "Check answers";
const reset = document.createElement("button");
reset.type = "button";
reset.className = "fcpython-quiz-secondary fcpython-quiz-reset";
reset.textContent = "Reset";
const score = document.createElement("p");
score.className = "fcpython-quiz-score";
score.setAttribute("aria-live", "polite");
check.addEventListener("click", () => {
let correctCount = 0;
let firstQuestionToReview = null;
quiz.questions.forEach((question, questionIndex) => {
const selected = container.querySelector(
`input[name="${quiz.id}-${question.id}"]:checked`
);
const feedback = feedbackNodes[questionIndex];
if (!selected) {
feedback.textContent = "Choose an answer before checking.";
feedback.className = "fcpython-quiz-feedback";
setStepStatus(questionIndex, "unanswered");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
return;
}
const selectedIndex = Number(selected.value);
if (selectedIndex === question.answer_index) {
correctCount += 1;
feedback.textContent = `✅ Correct. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-correct";
setStepStatus(questionIndex, "correct");
} else {
const answer = question.options[question.answer_index];
feedback.textContent = `❌ Not yet. Correct answer: ${answer}. ${question.explanation}`;
feedback.className = "fcpython-quiz-feedback is-incorrect";
setStepStatus(questionIndex, "incorrect");
if (firstQuestionToReview === null) {
firstQuestionToReview = questionIndex;
}
}
});
score.textContent = `Score: ${correctCount}/${quiz.questions.length}`;
if (firstQuestionToReview !== null) {
showQuestion(firstQuestionToReview, true);
}
});
reset.addEventListener("click", () => {
container.querySelectorAll("input[type='radio']").forEach((input) => {
input.checked = false;
});
feedbackNodes.forEach((feedback) => {
feedback.textContent = "";
feedback.className = "fcpython-quiz-feedback";
});
stepButtons.forEach((_, questionIndex) => {
setStepStatus(questionIndex, "");
});
score.textContent = "";
showQuestion(0, true);
});
actions.appendChild(previous);
actions.appendChild(next);
actions.appendChild(check);
actions.appendChild(reset);
container.appendChild(actions);
container.appendChild(score);
showQuestion(0);
return container;
}
FreeCampus Python
Debug One Hypothesis at a Time
Reproduce wrong behavior, localize its first divergence, test one falsifiable hypothesis, repair the cause, and verify against regressions.
python-foundations
errors-exceptions-debugging
debugging-method
logic-bugs
Course progress
0%
A weekend tournament awards points for wins and a streak bonus for consecutive wins. The following function produces a plausible total, raises no exception, and is wrong:
The correct total is 11: three wins contribute nine points, and only the second consecutive win contributes a two-point streak bonus. The function returns 17. This lesson is about the work between “wrong” and “fixed.”
Keep one rule throughout: each experiment should distinguish between possible causes. Editing several lines and seeing a pass tells you less than changing one cause and predicting exactly what new evidence should appear.
1. Freeze the failure before touching the code
A reproducible bug has a known starting state, action, expected result, and actual result. Turn the report into an assertion:
Do not repair yet. Record:
If a report cannot be reproduced, that is evidence—not permission to guess. Check the exact input representation, random seed, call order, Python version, saved file, working directory, configuration, and notebook state. Change one environmental factor at a time.
2. Establish a control case and nearby boundaries
A control is a simple case expected to work. It shows which parts of the program are not yet implicated:
Now try the smallest win case:
The contract says one win earns 3, but the function returns 5. That smaller failure removes loss/reset behavior from suspicion. The defect is already present on the first win.
Build a behavior table before editing:
| Input | Expected | Actual before repair | Rule isolated |
|---|---|---|---|
[] |
0 | 0 | empty tournament |
['L'] |
0 | 0 | a loss scores nothing |
['W'] |
3 | 5 | first win has no streak bonus |
['W', 'W'] |
8 | 12 | second win adds one bonus |
['W', 'L', 'W'] |
6 | 10 | loss resets the streak |
The one-win case is a minimal reproduction: removing its only event makes the failure disappear. Smaller input reduces the number of states you must explain.
TipA smaller failure is a sharper question
Minimization is not only for bug reports. It is an investigation tool. Remove one item, branch, function call, or configuration detail and ask whether the same rule still fails.
3. Separate facts from hypotheses
Facts come directly from observations or the contract:
tournament_score(["W"])returns5on a clean run;- the rule awards
3for a first win; - no loss branch runs for that input;
streakbegins at zero.
Hypotheses are proposed explanations:
- H1:
streakis not initialized to zero. - H2: the bonus formula applies to the first win instead of only later wins.
- H3: the loss branch incorrectly adds points.
H1 and H3 conflict with visible evidence: the source initializes zero, and the minimal case never enters the loss branch. H2 predicts exactly the observed two extra points.
A useful hypothesis names a mechanism and a predicted observation:
If the formula uses the new streak length directly, then the first win will add
2 * 1; printing the base and bonus separately will show base3, bonus2before total becomes5.
“The loop is weird” cannot be meaningfully disproved. “The first win receives a bonus because the calculation uses streak rather than streak - 1” can.
Checkpoint: make the failure testable
4. Localize the first wrong stage
For larger programs, identify the first stage where actual behavior differs from expected behavior. Model the scorer as a pipeline:
The input is already a clean list, iteration sees the correct first item, and the streak becomes one as expected. The first divergence is bonus calculation. Do not edit parsing, iteration, or reset code when their observed states still match the contract.
Debugging moves from a broad failing outcome toward the first state transition that violates a known rule.
flowchart LR report["Wrong final total"] --> pipeline["Name program stages"] pipeline --> boundary["Compare expected and actual after each stage"] boundary --> first["Find first divergence"] first --> hypothesis["Predict a local cause"] hypothesis --> experiment["Observe or change one thing"] experiment --> verify["Rerun original and nearby cases"]
Later wrong values can be consequences. Repair the earliest supported cause, not every downstream symptom.
Function boundaries provide natural checkpoints. Suppose a leaderboard has three stages:
def normalize_results(results):
return [result.strip().upper() for result in results]
def score_results(results):
return tournament_score(results)
def build_summary(raw_results):
normalized = normalize_results(raw_results)
score = score_results(normalized)
return {"results": normalized, "score": score}Check outputs at each boundary:
If normalization passes and scoring fails, the first broken boundary lies after normalization. That does not prove every normalizer case is correct; it localizes this reproduction.
5. Trace state when the problem lives inside a loop
A state trace records selected values after each meaningful transition. Create an instrumented version without changing the scoring decisions:
def trace_tournament_score(results):
total = 0
streak = 0
trace = []
for turn, result in enumerate(results, start=1):
before_total = total
if result == "W":
streak += 1
base = 3
bonus = 2 * streak
total += base + bonus
else:
streak = 0
base = 0
bonus = 0
trace.append(
{
"turn": turn,
"result": result,
"streak": streak,
"base": base,
"bonus": bonus,
"before": before_total,
"after": total,
}
)
return total, trace
actual, trace = trace_tournament_score(["W"])
print(actual)
print(trace[0])The trace should show base: 3, bonus: 2, after: 5. That observation supports H2. It also falsifies an alternative claim that addition happens twice in separate statements.
Choose only variables needed for the hypothesis. Good instrumentation might show a loop index, current item, branch chosen, before value, delta, and after value. Printing entire nested objects on every iteration can bury the transition you need.
Remove or convert temporary prints after the repair. Preserve the valuable rule as an assertion so future runs check it automatically.
6. Change one cause and predict the result
The contract says a streak bonus applies only to wins after the first. If streak is one, bonus count should be zero; if streak is two, bonus count should be one. Repair only that formula:
Before running, predict:
['W']becomes3;['W', 'W']becomes8;- the original case becomes
11; - loss-only controls remain zero.
Then execute the exact checks:
A matching prediction is evidence that the causal model improved. If the original case passes but a control regresses, the change is not complete.
Checkpoint: locate before you repair
7. Repair the cause, not the visible symptom
A symptom patch could subtract six from the original result:
It might match one reported case while breaking an empty tournament and every other shape. Another weak patch might special-case the exact failing list. Both encode examples rather than the streak rule.
A root-cause repair changes the earliest wrong decision supported by evidence. It should explain why all nearby cases now work:
- first win:
streak - 1is zero; - second consecutive win: it is one;
- a loss resets streak to zero;
- a later isolated win again receives no bonus.
After repairing, ask what other behavior shares the changed line. Test long streaks, repeated losses, empty input, and alternating results. A fix can solve the report while exposing a neighboring boundary.
8. Keep regression evidence and clean the investigation
A regression check is a small executable example that would fail if the defect returned. The strongest one is often the minimal reproduction:
Unit 13 will organize these functions with pytest. Right now, naming the rule and running the assertion is enough to preserve learning.
Complete the repair with a clean verification:
- remove unrelated experimental changes;
- keep a concise assertion for the original defect;
- run the minimal case, original case, controls, and nearby boundaries;
- restart the notebook runtime and run top to bottom, or rerun the saved script;
- record the cause and why the repair matches the contract.
Do not describe a repair only as “changed line 8.” Line numbers move. Record the behavioral reason: “The formula treated the first win as an additional streak win; subtracting one from the streak count makes bonuses begin at win two.”
9. Intermittent failures need controlled repetition
Suppose a game selects a random prize and a test sometimes fails. Repeatedly clicking Run creates observations but not a stable experiment. Control the random input or inject it:
For time-dependent, network, or environment-dependent behavior, record the varying dependency and substitute a controlled value when the design permits. Unit 13 covers test doubles and dependency injection more fully.
10. Use AI suggestions as hypotheses, not evidence
An assistant can propose suspicious lines, smaller examples, or possible assertions. It cannot observe your exact runtime unless you supply accurate, safe evidence, and a plausible explanation can still be wrong.
Use this workflow:
- remove secrets and unrelated private data;
- provide the minimal complete reproduction, exact traceback or output, expected behavior, environment, and attempts already made;
- ask for several falsifiable hypotheses and an experiment for each;
- inspect every suggested change before running it;
- run one controlled experiment locally;
- keep the repair only when original and regression cases support it.
Never paste tokens, passwords, private student records, or proprietary data into an external tool. Replace values while preserving the shape that triggers the failure.
11. Investigate a second scoreboard defect
The tournament now supports a three-point comeback bonus after a loss followed by two wins. This implementation silently awards it too soon:
The rule says ['L', 'W'] earns 3, while ['L', 'W', 'W'] earns 9. Conduct the investigation rather than jumping to a patch:
- freeze both assertions and record actual results;
- find a passing control;
- shrink any longer reported failure;
- list facts separately from at least three hypotheses;
- trace
result,previous,wins_after_loss, andtotalper turn; - mark the first divergence;
- run one experiment that falsifies a hypothesis;
- make one root-cause repair;
- verify empty, loss-only, isolated-win, qualifying-comeback, and repeated-win cases from a clean state.
Use this evidence table:
| Stage | Expected | Actual | Evidence | Next hypothesis |
|---|---|---|---|---|
| reproduction | ||||
| smallest case | ||||
| first divergence | ||||
| controlled experiment | ||||
| clean verification |
Compare a root-cause repair after completing the investigation
One clear model remembers whether a loss has armed a comeback and counts wins until the two-win requirement is satisfied:
def comeback_score(results):
total = 0
comeback_armed = False
wins_after_loss = 0
for result in results:
if result == "L":
comeback_armed = True
wins_after_loss = 0
continue
total += 3
if comeback_armed:
wins_after_loss += 1
if wins_after_loss == 2:
total += 3
comeback_armed = False
wins_after_loss = 0
return total
assert comeback_score([]) == 0
assert comeback_score(["W"]) == 3
assert comeback_score(["L", "W"]) == 3
assert comeback_score(["L", "W", "W"]) == 9
assert comeback_score(["L", "W", "W", "W"]) == 12This is one contract-consistent design, not the only syntax that can work. Your record should still show which observation falsified a hypothesis and why the state stops awarding the comeback after it is earned.
Checkpoint: close the investigation responsibly
12. Key points and your reusable debugging record
For future lessons and projects, keep this compact template:
The record is not bureaucracy. It prevents repeated guesses, supports a teammate who joins later, and makes your own reasoning inspectable.
References and next steps
Next, you will pause a running program, inspect stack frames and local variables, step across function calls, and reduce a realistic report into a minimal reproduction someone else can run.