{
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
Combining Conditions Without Surprises
Turn several facts into readable Boolean rules, use short-circuit evaluation as a safety guard, and avoid collapsing meaningful falsy states.
python-foundations
decisions-repetition
boolean-logic
Course progress
0%
1. Name the facts before combining the rule
A museum security console unlocks an archive only for an authorized visitor when the room is open and no alarm is active. One dense condition can express that, but named facts let a reader inspect the reasoning:
Each comparison or membership test creates a Boolean. The final rule combines facts whose names say what they mean. If entry is unexpectedly denied, the three printed values immediately narrow the cause.
Unit 2 established how comparisons, truthiness, and, or, and not work. Here you will use those rules to design control-flow questions that remain readable at boundaries and safe when some operations are not always valid.
Questions this lesson will answer
- How do truth tables expose a rule’s complete behavior?
- Where do parentheses matter when
andandorappear together? - How can evaluation order prevent an invalid lookup or index?
- Why do
None,0,"", and[]sometimes need different outcomes? - When do
all()andany()make a collection of facts clearer?
2. Truth tables turn words into evidence
For two facts, enumerate every combination instead of reasoning from one happy case:
is_authorized |
is_open |
is_authorized and is_open |
is_authorized or is_open |
|---|---|---|---|
False |
False |
False |
False |
False |
True |
False |
True |
True |
False |
False |
True |
True |
True |
True |
True |
andrequires every combined requirement.oraccepts at least one alternative.notreverses one truth value.
Translate policy words deliberately:
| Policy wording | Typical Boolean shape |
|---|---|
| “both,” “all,” “must also” | A and B |
| “either,” “at least one” | A or B |
| “unless,” “not,” “no alarm” | often includes not A |
| “exactly one” | (A and not B) or (B and not A) |
Do not assume everyday “or” means exactly one. Python’s or is inclusive: it is true when one or both sides are true.
The first result is true; the second is false because both alternatives are present.
3. Parentheses make mixed rules visible
Python evaluates not before and, and and before or. Therefore:
means:
A staff member can enter even while closed. If the intended rule is “the room must be open, and the person must be staff or have a ticket,” write:
The parentheses are not decoration. They identify one grouped idea before it is combined with another. Even when precedence would produce the same answer, use parentheses when the policy has an obvious phrase boundary.
A quick review technique is to substitute Boolean values:
Both happen to be false here, so also test a case that distinguishes the shapes:
Checkpoint: translating policies
4. Short-circuiting can protect an unsafe lookup
Python evaluates and from left to right. If the left side is falsy, the whole and expression cannot become truthy, so Python skips the right side:
bool(codes) is false, so codes[0] is never evaluated. No IndexError occurs. Reverse the operands and the guard arrives too late:
Python attempts the unsafe index first and raises IndexError. A guard must be on the left of the operation it protects.
A dictionary example uses membership before indexing:
The membership test is false, so the missing key lookup is skipped. This is a control-flow effect inside an expression: not every written operand necessarily runs.
ImportantA safety guard should prove the next operation is valid
data and data[0] protects an index because a truthy sequence has at least one item. "pass" in visitor and visitor["pass"] == "active" protects a dictionary lookup because membership proves the key exists.
5. or also stops when the outcome is known
For or, a truthy left operand already determines success, so Python skips the right side:
The code comparison is not needed. Short-circuiting is observable when the right side would fail:
This runs because the lookup is skipped. That does not mean missing settings are always acceptable; it means the policy explicitly permits the override to make the other fact irrelevant.
Do not hide important work behind and or or merely to save lines. Use a normal conditional when the skipped action changes state, needs an explanation, or would surprise a reader.
6. and and or return operands, not forced Booleans
Unit 2 introduced this rule. It matters when a condition also constructs data:
Because label is falsy, or returns the second operand string. If label were "Meteor Map", it would be returned unchanged.
This defaulting pattern is safe only when every falsy first value means “absent.” It is wrong if 0 is meaningful:
Preserve the distinction explicitly:
When you need a real Boolean for state or output, use a comparison or bool(...) instead of relying on the returned operand.
Checkpoint: evaluation order and guards
7. De Morgan’s laws help invert a complete rule
Suppose entry is allowed when a visitor is authorized and the alarm is not active:
The denial condition is the negation of that whole expression:
De Morgan’s laws give an equivalent form:
The two transformations are:
not (A and B)is equivalent to(not A) or (not B);not (A or B)is equivalent to(not A) and (not B).
Notice that the connective changes as each fact is negated. Test all Boolean combinations when an inversion controls access, safety, payment, or another important policy.
This small exhaustive check is possible because two Boolean facts have only four combinations.
8. Impossible ranges reveal a broken rule
Some conditions can never be true:
No one value can be below 13 and at least 18 simultaneously. Perhaps the intended condition was age < 13 or age >= 18, describing values outside 13 through 17.
Other rules are always true:
Every number satisfies at least one side, including the overlapping middle. Writing a few boundary values—12, 13, 17, and 18—usually reveals the problem. Lesson 3 expands this technique into decision tables.
9. all() and any() combine a collection of facts
When facts already exist as a collection, built-ins can communicate the rule:
all(checks)is true only if every item is truthy.any(checks)is true if at least one item is truthy.
They also short-circuit while visiting the collection: all() can stop at the first falsy item; any() can stop at the first truthy item.
The empty cases are deliberate:
“All zero checks passed” is true because no failing check exists. “At least one of zero checks passed” is false because no successful check exists. Whether an empty collection should be accepted by your application is a separate policy; you can require both non-emptiness and all(checks):
Checkpoint: complete Boolean reasoning
10. Build a museum security console
Use these inputs and create named facts rather than one unreadable condition:
The policy is:
- The museum is open from hour 9 through 16; hour 17 is closed.
- The visitor is authorized when the role is
"curator"or"researcher". - A pass is valid only if the
"pass"key exists and equals"active". - No one enters while the alarm is active.
- A restricted exhibit additionally requires either a curator or an escort.
- Assign Boolean
accessand one specificreason. Check general failures in this order: closed museum, alarm, authorization/pass, restricted access. - Do not index the pass before proving its key exists.
Run these checks for the supplied case:
Then try a missing pass, hour = 17, an active alarm, and a researcher at the restricted exhibit without an escort. For each case, predict the first failing fact and the resulting reason.
Hint: build facts from simple to dependent
Create is_open, is_authorized, and has_active_pass separately. The pass fact should use membership on the left of and. Define the restricted-exhibit fact so that unrestricted exhibits pass without needing an escort. Use a branch chain to choose the most useful reason.
Show one complete solution after attempting the lab
visitor = {"role": "researcher", "pass": "active"}
hour = 16
alarm_active = False
restricted_exhibit = True
escort_present = True
is_open = 9 <= hour < 17
is_authorized = visitor["role"] in {"curator", "researcher"}
has_active_pass = (
"pass" in visitor
and visitor["pass"] == "active"
)
restriction_satisfied = (
not restricted_exhibit
or visitor["role"] == "curator"
or escort_present
)
if not is_open:
access = False
reason = "museum closed"
elif alarm_active:
access = False
reason = "alarm active"
elif not (is_authorized and has_active_pass):
access = False
reason = "authorization failed"
elif not restriction_satisfied:
access = False
reason = "escort required"
else:
access = True
reason = "access granted"The branch chain reports one precedence-ordered reason. The named facts preserve which part of the policy each expression represents.
11. Explain the choices
- Why are named Boolean facts easier to debug than one dense condition?
- How can parentheses change a policy that mixes
andandor? - What must a left-hand guard prove before Python reaches an unsafe right side?
- Why is
value or defaultunable to preserve every valid zero? - What do
all([])andany([])mean, and when might a policy additionally require a non-empty input?
Key points
TipKey points
- Name meaningful facts, then combine those names into the policy.
- Parenthesize mixed
and/orrules according to their logical phrases. andskips its right operand after a falsy left operand;orskips it after a truthy left operand. Put safety guards before the operations they protect.- Truthiness can collapse meaningful states such as zero and
None; compare explicitly when the distinction matters. - De Morgan’s laws invert a whole rule, and boundary cases reveal impossible or always-true ranges.
all()andany()combine collections of facts and have deliberate empty behavior.