{
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
Pause, Inspect, and Shrink a Bug
Choose a debugging tool, pause before a suspicious line, inspect frames and state, and reduce a failure to a safe complete reproduction.
python-foundations
errors-exceptions-debugging
debugger
minimal-reproduction
Course progress
0%
A robot courier should deliver three parcels and finish with seven energy points. Instead, it finishes with four:
def delivery_cost(distance, fragile):
"""Return energy spent for one delivery."""
cost = distance * 2
if fragile:
cost += 1
return cost
def run_route(parcels, starting_energy):
"""Return remaining energy after delivering every parcel."""
energy = starting_energy
for parcel in parcels:
cost = delivery_cost(parcel["distance"], parcel["fragile"])
energy -= cost
energy -= 1
return energy
parcels = [
{"name": "map", "distance": 1, "fragile": False},
{"name": "lamp", "distance": 2, "fragile": True},
{"name": "key", "distance": 1, "fragile": False},
]
assert run_route(parcels, 16) == 7You could add prints. You could pause execution. You could shrink the route to one parcel. Good debugging is not loyalty to one tool; it is choosing the least complicated tool that can answer the current question.
1. Match the tool to the uncertainty
| Tool | Best question | Strength | Risk |
|---|---|---|---|
| traceback | where did an exception stop, and through which calls? | already produced, structured | absent for silent wrong results |
focused print/repr |
what value reaches this point? | fast and portable | noisy output or accidental state changes |
| assertion | which rule first becomes false? | executable and reusable | needs a known expected condition |
| state trace | how does selected state change across a loop? | exposes transitions | can collect too much data |
| debugger | what are locals and call frames at this exact moment? | pause, inspect, step without adding many prints | stepping without a question becomes wandering |
| minimal reproduction | what is essential to trigger the failure? | removes noise and aids sharing | removing too much can remove the cause |
Start with the smallest question. If one assertion identifies the bad transition, a debugger is unnecessary. If values change across nested calls and a loop, pausing and stepping may be clearer than adding fifteen prints.
TipWrite the debugger question first
Examples: “What is cost immediately before energy changes?” or “Which call first turns energy below the expected value?” A breakpoint without a question is only a pause.
2. A breakpoint pauses before the highlighted line executes
Save the opening example as courier.py, including a final print:
In VS Code, select the gutter beside this line inside run_route:
Start a Python debugging session. When execution reaches the breakpoint, that line has not run yet. Inspect:
parcel: the current mapping;cost: the value returned bydelivery_cost;energy: the value before subtraction;- the call stack: module code called
run_route, which is now paused.
That before/after distinction prevents off-by-one confusion. Step over the line once, then compare energy again.
The four movements through paused code
Debugger interfaces use similar actions:
- Continue runs until another breakpoint, exception pause, or program end.
- Step over executes the current line and pauses at the next line in the current frame; called functions run without pausing inside them.
- Step into enters a function called by the current line so you can inspect its first executable step.
- Step out finishes the current function and pauses back in its caller.
For the courier:
- pause before the call to
delivery_cost; - step into it when you need to verify its formula;
- step out once the return value is understood;
- step over each energy subtraction;
- continue to the next parcel after recording the transition.
Stepping every library line is rarely useful. Move deliberately between application frames that can answer your hypothesis.
3. Inspect locals and the call stack together
A local variable makes sense in the context of its frame. If paused inside delivery_cost, locals include distance, fragile, and cost. The caller’s frame contains parcel and energy.
Use the call-stack panel to select each frame and compare the interface:
| Frame | Inputs or locals to inspect | Contract question |
|---|---|---|
delivery_cost |
distance, fragile, cost |
does one parcel cost match the formula? |
run_route |
parcel, cost, energy |
is the returned cost applied exactly once? |
| module | parcels, starting value |
did the scenario supply the intended data? |
Expressions entered in a debug console can have side effects. Prefer simple observations such as repr(parcel), cost, energy, or type(parcel).__name__. Avoid calling methods that mutate a list or mapping unless mutation is the planned experiment.
WarningInspect before you edit state
Changing a variable in the debug console can make the current run pass without repairing the source. If you deliberately modify state, record it as an experiment and restart before verifying the actual fix.
Checkpoint: control the pause
4. Stop only on the iteration that matters
The third parcel may be the only failing case in a hundred-item route. Clicking Continue ninety-nine times is not an investigation strategy. Use a conditional breakpoint when your editor supports it, for example:
The debugger evaluates that condition at the breakpoint and pauses only when it is true. Other useful conditions include:
Keep conditions free of side effects. A condition should observe the program, not modify it.
You can also encode a temporary condition in source:
The source version is portable and easy to show in a notebook. An editor conditional breakpoint keeps production logic untouched. Choose based on the environment and whether the observation should remain in code.
5. Use breakpoint() and pdb in a local script
Python’s built-in breakpoint() enters the configured debugger at that call site. Put it in a local script only while investigating:
Do not run that cell in hosted course notebooks unless you know how the environment handles interactive debugging. Save a complete script and run it locally instead.
You can also start any saved script under Python’s standard debugger without editing the file:
At the (Pdb) prompt, these commands cover a focused first session:
| Command | Meaning |
|---|---|
l |
list source around the current line |
p expression |
print one expression |
pp expression |
pretty-print one expression |
n |
next line in the current frame (step over) |
s |
step into a called function |
r |
run until the current function returns (step out) |
c |
continue until another stop |
where |
show the current stack |
up / down |
select an older/newer frame |
q |
quit the debugging session |
A short session might look like:
The command letters are less important than the reasoning: pause before the transition, inspect inputs, execute one operation, inspect the after-state, and compare it with the expected transition.
Remove stray breakpoint() calls after investigating. A forgotten breakpoint can halt another user’s run unexpectedly.
6. The courier’s first divergence
Trace the opening route without changing it:
| Parcel | Cost returned | Energy before | Expected after | Actual after |
|---|---|---|---|---|
| map | 2 | 16 | 14 | 13 |
| lamp | 5 | 13 | 8 | 7 |
| key | 2 | 7 | 5 | 4 |
For the code shown here, the first divergence occurs immediately after the correct delivery cost is applied: an extra energy -= 1 charges an undocumented fee.
Repair the earliest wrong transition:
Calculate the oracle independently: delivery costs are 2, 5, and 2, totaling 9, so starting at 16 leaves 7. If an issue report had claimed the answer should be 6, this calculation and the per-parcel trace would challenge the report itself. Debugging includes verifying the oracle.
Use corrected evidence:
This is a realistic lesson: user reports, comments, and assertions can be wrong. When actual transitions consistently contradict the stated expectation, return to the contract and recalculate it independently before forcing code to match.
Checkpoint: trust evidence, verify the oracle
7. A minimal reproduction is small and complete
Imagine the real courier application has menus, colors, files, twenty parcel fields, and logging. The failure only depends on distance, fragility, starting energy, and the extra subtraction. A useful reproduction keeps those pieces:
def delivery_cost(distance, fragile):
cost = distance * 2
if fragile:
cost += 1
return cost
def buggy_remaining_energy(distance, fragile, starting_energy):
cost = delivery_cost(distance, fragile)
energy = starting_energy - cost
energy -= 1
return energy
assert buggy_remaining_energy(1, False, 16) == 14It is:
- minimal enough to expose one parcel and one extra transition;
- complete because every name, function, input, and expected value needed to run is included;
- deterministic because the same input produces the same failure;
- safe to share because it contains no secret paths, customer addresses, or access tokens.
A fragment such as energy -= 1 is small but not complete. A full production repository may be complete but not minimal. Aim for the smallest standalone program that preserves the same behavior.
8. Reduce one dimension at a time
Start from a copied reproduction, not the only production artifact. Confirm it still fails, then reduce deliberately:
- remove unrelated parcels;
- remove unused mapping fields;
- replace file or network input with a literal value of the same relevant shape;
- inline a helper only if the failure remains;
- remove formatting, UI, or logging;
- rerun after every reduction;
- restore the last removed element if the failure disappears.
Track results:
| Reduction | Prediction | Still fails? | Conclusion |
|---|---|---|---|
| keep one map parcel | extra charge should remain | yes | multiple parcels are unnecessary |
remove name |
calculation should remain | yes | name is irrelevant to this failure |
replace fragile with False |
extra charge should remain | yes | fragile branch is not required |
| remove second subtraction | failure should disappear | no | subtraction is causally required |
The last row is both a reduction and a controlled experiment. It connects one line to the observed mismatch.
WarningDo not reduce away the environment too early
If a bug depends on Python version, operating system, package version, working directory, locale, timing, or call order, that detail is part of the reproduction. Minimal means no irrelevant pieces, not no context.
9. Write a bug report someone else can run
A useful report includes:
Title: Courier charges an extra energy point per parcel
Environment:
- Python version:
- operating system/editor if relevant:
- clean command used to run:
Steps:
1. Save the attached complete example as courier_minimal.py.
2. Run `python courier_minimal.py`.
Expected:
One non-fragile distance-1 delivery costs 2; 16 energy should leave 14.
Actual:
The assertion fails because the function returns 13.
First divergence:
Cost is correctly 2. Energy becomes 14, then an additional subtraction makes 13.
Attachment:
Minimal complete example with synthetic, non-sensitive data.Include the exact traceback for an exception or repr of surprising values. Do not paraphrase TypeError as “it crashed” or silently edit the message.
Sanitize without changing shape. Replace a real path with /example/project/data.txt, a token with <redacted-token>, and personal data with synthetic records. Rerun the sanitized reproduction to prove it still fails.
10. Run a debugger-to-reproduction lab
The courier gets a new rule: fragile parcels cost one extra point only when distance is greater than one. The implementation applies the fee to every fragile parcel:
def delivery_cost(distance, fragile):
cost = distance * 2
if fragile:
cost += 1
return cost
route = [
{"name": "glass key", "distance": 1, "fragile": True, "color": "blue"},
{"name": "lamp", "distance": 3, "fragile": True, "color": "gold"},
{"name": "map", "distance": 4, "fragile": False, "color": "green"},
]Complete the lab:
- write expected costs for all three parcels;
- save a complete local script and reproduce the wrong first cost;
- pause before
cost += 1and inspectdistanceandfragile; - use a condition to pause only when
distance == 1; - step over the fee and record before/after state;
- reduce to one function call and one assertion;
- remove
name,color, and the other parcels one at a time, rerunning each time; - state a falsifiable hypothesis;
- repair the condition and add checks for both sides of the boundary;
- remove temporary breakpoints and run the saved script normally.
Target regression checks:
Do not copy those expected values blindly. Explain each from the updated rule.
Show the minimal repair after finishing the debugger trace
The fee needs both conditions:
The one-parcel failure delivery_cost(1, True) == 2 is the most focused regression. The distance-two and non-fragile checks protect the neighboring branches. Remove debugger stops, save the script, and run all four checks in a normal process before comparing your result.
Checkpoint: shrink without losing the cause
11. Key points for the arcade challenge
- Choose the simplest tool that can answer the current uncertainty.
- A breakpoint normally pauses before its highlighted line; inspect, step once, and compare the after-state with a predicted transition.
- Use frames to understand caller and callee state, and conditional breakpoints to reach the relevant iteration.
breakpoint()andpython -m pdb script.pyare useful local options; remove temporary stops after the investigation.- Verify the oracle as carefully as the implementation.
- A minimal reproduction must be small, complete, deterministic, safe, and independently runnable.
- Reduce one dimension at a time and rerun after every change.
References and next steps
- Python documentation:
pdb— The Python Debugger - Python documentation: built-in
breakpoint() - VS Code documentation: Python debugging
- VS Code Python tutorial: run and debug
The unit challenge supplies a small arcade with several independent defects. Progressive assertions and a hint ladder will help you isolate parsing, movement, and exception-boundary behavior without hiding unexpected failures.