{
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
Searching, Skipping, and Stopping Loops
Design searches for first, last, all, or existence results; use break, continue, and loop else deliberately; and preserve an explicit exit reason.
python-foundations
decisions-repetition
loop-control
Course progress
0%
1. Define what “find” means before searching
A signal scanner receives several readings:
“Find B7” is incomplete. The required artifact might be:
- the first matching record;
- the last matching record;
- all matching records;
- whether any match exists; or
- the position of the first match.
Each contract implies different loop behavior. A first-match search can stop as soon as it has sufficient evidence:
break exits the loop immediately. The final record is never inspected, which is correct only because later matches cannot change the first result.
Questions this lesson will answer
- When should a search stop, and when must it inspect every item?
- How does
continuediffer frombreakandpass? - What does a loop’s
elsereally mean? - Which loop receives a
breakinside nested code? - When should membership or
any()replace a manual search?
2. First, last, and all matches require different updates
A last-match search must keep traversing:
Every new match replaces the prior one. An all-match search collects instead:
An existence search needs only a Boolean:
State the no-match result too: None for no selected record, [] for no collected records, and False for no existence evidence. Empty input should produce the same defined values without special repair after the loop.
3. continue rejects one iteration, not the whole search
Suppose malformed records lack a usable code:
signals = [
{"code": ""},
{"strength": 8},
{"code": "B7", "strength": 9},
]
rejected = []
first_match = None
for position, signal in enumerate(signals):
if "code" not in signal or not signal["code"]:
rejected.append((position, signal))
continue
if signal["code"] == "B7":
first_match = signal
break
assert len(rejected) == 2
assert first_match == signals[2]continue skips the rest of the current body and asks the for loop for its next item. Recording the rejection before continuing prevents silent loss.
break would be wrong for a bad first record because later valid records still need inspection. Conversely, continue would be wrong after a first-match success because it would keep searching after the contract is satisfied.
Checkpoint: search contracts
4. Loop else means no break occurred
A loop may have an else aligned with it:
The else runs only if the loop finishes without break. It does not mean the final if condition was false. Several nonmatching iterations may occur, but a later break still skips the loop else.
For no match:
An empty collection also reaches else because no break occurred.
TipAttach the meaning to
break
Read loop else as “normal exhaustion.” Use it when break has one clear meaning such as success. If several unrelated breaks exist, an explicit exit-reason name may be easier to understand.
5. A while loop can also have else
The else runs if the condition becomes false normally. A break skips it. As in Lesson 5, every path that stays in the loop must advance index; here the break path exits, and the nonmatch path increments.
6. Record why a loop exited
One scanner can encounter success, shutdown, exhaustion, or an invalid limit:
commands = ["noise", "target", "shutdown"]
exit_reason = "exhausted"
found = None
for command in commands:
if command == "shutdown":
exit_reason = "shutdown"
break
if command == "target":
found = command
exit_reason = "target found"
break
assert exit_reason == "target found"
assert found == "target"Initialize the normal-exhaustion reason before the loop; replace it immediately before each break. This leaves an explicit result after all paths.
A loop else would also work for exhaustion, but it would not distinguish two kinds of break by itself. Choose the form that makes exit states most visible.
Checkpoint: loop else and exit reasons
7. break exits only the nearest loop
Nested loops make the target important:
Each break exits only the inner loop. The outer loop proceeds to the next group. If the whole search should stop, preserve a flag and check it in the outer loop:
Lesson 7 applies this behavior to grids.
8. pass does nothing; it does not skip the body
pass is a placeholder statement:
All three values print. For value 2, pass performs no action, then execution continues with the next statement in the same body.
With continue, the value 2 would not print:
Use pass only when Python requires a statement but the block is intentionally empty during development or by design. It is not a synonym for “skip this item.”
9. Prefer direct operations for direct questions
Manual loops are excellent for learning and for collecting rich evidence. When the final question is already a built-in operation, say it directly:
- Membership answers whether an equal value exists.
any(...)answers whether at least one generated fact is truthy.all(...)answers whether every generated fact is truthy.
The expressions inside any and all are generator expressions, a lazy form previewed here because the built-ins consume it directly. Lesson 8 focuses on comprehensions and explains why square brackets would unnecessarily build an intermediate list for this yes/no question.
Use an explicit loop when you need the matching record, position, rejection log, or exit reason—not only a Boolean.
Checkpoint: control statements and direct questions
10. Build a signal scanner
Use this ordered signal stream:
signals = [
{"code": "", "strength": 8},
{"strength": 9},
{"code": "A1", "strength": 4},
{"code": "B7", "strength": 3},
{"code": "B7", "strength": 9},
{"code": "SHUTDOWN", "strength": 0},
{"code": "B7", "strength": 10},
]
target = "B7"
minimum_strength = 7
rejected = []
first_match = None
first_position = None
exit_reason = "exhausted"Implement this contract:
- Reject a record when the code key is absent or the code is empty; preserve its position and reason, then continue.
- Stop with reason
"shutdown"when code is"SHUTDOWN". - A target qualifies only when strength is at least the minimum.
- Stop at the first qualifying target, preserving the record and its position.
- If traversal exhausts normally, keep reason
"exhausted". - Preserve the source list unchanged.
Run:
Then test no target, shutdown before a target, an empty list, and a qualifying target in the first position. Explain whether loop else or the initialized exit reason better communicates your implementation.
Hint: order rejection, shutdown, and success checks
Use enumerate(signals) and make the missing-key check before indexing code. Give missing and empty codes distinct rejection tuples. Set exit_reason immediately before each break. A loop else can explicitly restore or confirm "exhausted".
Show one complete solution after attempting the lab
signals = [
{"code": "", "strength": 8},
{"strength": 9},
{"code": "A1", "strength": 4},
{"code": "B7", "strength": 3},
{"code": "B7", "strength": 9},
{"code": "SHUTDOWN", "strength": 0},
{"code": "B7", "strength": 10},
]
target = "B7"
minimum_strength = 7
rejected = []
first_match = None
first_position = None
exit_reason = "exhausted"
for position, signal in enumerate(signals):
if "code" not in signal:
rejected.append((position, "missing code"))
continue
if not signal["code"]:
rejected.append((position, "empty code"))
continue
if signal["code"] == "SHUTDOWN":
exit_reason = "shutdown"
break
if signal["code"] == target and signal["strength"] >= minimum_strength:
first_match = signal
first_position = position
exit_reason = "target found"
break
else:
exit_reason = "exhausted"The two target records at positions 3 and 4 demonstrate that matching the code is not enough; the strength rule still decides qualification.
11. Explain the stopping decision
- Why can a first-match loop stop but an all-match loop cannot?
- What evidence should be recorded before
continuediscards the current path? - Why does loop
elsemean no break rather than “the final if was false”? - How would you stop both loops in a nested search?
- When do membership,
any(), orall()express the entire question?
Key points
TipKey points
- Define whether the result is first, last, all, existence, or position before choosing loop control.
breakexits the nearest loop;continueskips the remaining current body;passdoes nothing.- A loop’s
elseruns after normal exhaustion and is skipped bybreak. - Record rejection evidence before continuing and an exit reason before breaking when several exit states matter.
- Use membership,
any(), orall()when a direct Boolean is the whole result; keep an explicit loop for richer artifacts.