{
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
Building Collections with Comprehensions
Translate explicit loops into readable list, set, and dictionary comprehensions while preserving evaluation order, collision behavior, scope, and readability.
python-foundations
decisions-repetition
comprehensions
Course progress
0%
1. A comprehension describes one new collection
An explicit loop can transform every reading:
A list comprehension expresses the same result:
Read it in this order:
for celsius in celsius_valuessupplies each source value;celsius * 9 / 5 + 32builds one output; and- the surrounding brackets collect those outputs into a new list.
The output expression appears first in the syntax, but the for clause tells you where its name comes from. A comprehension is useful when the complete result can be said as one short sentence: “the converted temperature for each source temperature.”
Questions this lesson will answer
- How does a trailing
iffilter inputs? - How is a conditional expression different from that filter?
- What do set and dictionary comprehensions collect?
- What happens when several inputs produce the same dictionary key?
- When does a comprehension become less readable than the loop it replaces?
2. Translate back to a loop whenever the order is unclear
For this comprehension:
The equivalent loop is:
Both produce ['Ada', '', 'Lin']. Write the expanded form when debugging:
- the source clause becomes the loop header;
- the leading expression becomes the value appended; and
- a trailing filter, if present, becomes an
ifaround the append.
The comprehension builds a new outer list. It does not mutate names.
3. A trailing if filters source items
Read the execution order as:
- receive a
reading; - check
reading >= 0; - only when true, evaluate and collect the leading
readingexpression.
Transformation and filtering can coexist:
The condition protects the output expression. For example, filter nonzero values before division:
No division occurs for zero.
Checkpoint: reading list comprehensions
4. A conditional expression chooses an output for every item
This form has an if and else before the for:
Every score produces exactly one label. Compare it with a trailing filter:
result_when_true if condition else result_when_falseis a conditional expression. It transforms every input into one of two outputs.... for item in source if conditionis a filter. It may omit the input.
The location and presence of else reveal the difference.
A conditional expression and filter can both appear:
First the source supplies a value. The trailing filter excludes None. Then the conditional expression chooses a label for each remaining value.
5. Set comprehensions collect unique results
Curly braces with one expression create a set comprehension. Multiple inputs can produce the same normalized value, and the set keeps it once. Do not rely on a set’s display order; use a list when first-seen order matters.
An empty set comprehension still returns a set:
The literal {} is an empty dictionary, so set() remains the syntax for a standalone empty set.
6. Dictionary comprehensions require a key and value
The expression before the for has key: value. The equivalent loop assigns one dictionary entry per source record:
A dictionary comprehension can transform both sides:
Checkpoint: output collection shapes
7. Duplicate dictionary keys keep the last assigned value
The second "north" assignment replaces the first. That may be a deliberate “latest wins” policy, but it is not grouping. If all records per station matter, use the explicit grouping loop from Lesson 4.
Before using a dictionary comprehension, ask whether keys are guaranteed unique, whether replacement is intended, and whether overwritten evidence must be preserved.
8. enumerate() and .items() remain available
Create a lookup from value to first-seen position only when source values are known unique:
Transform a mapping through its items:
The trailing filter checks the current quantity, and the key-value expression preserves accepted pairs.
9. Nested comprehensions follow nested-loop order
A small flattening comprehension mirrors two loops:
Read the for clauses in the same order as expanded loops:
A small Cartesian product is similar:
Stop compacting when there are several filters, branching actions, or unfamiliar names. The explicit loop is not inferior; it provides space for intermediate state, rejection evidence, traces, and comments.
10. Comprehension target names do not leak
In modern Python, the comprehension’s target has its own local scope and does not replace the outer value. Avoid relying on a comprehension target afterward; use the constructed collection.
An ordinary for target behaves differently at top level:
This difference is another reason to keep target names local in meaning and not use them as later results.
11. Do not use a comprehension only for side effects
This creates a list of None values merely to print:
The list is useless because print() returns None. Use a loop for actions:
Comprehensions build collections. They do not support break or continue, and they are a poor home for several state changes. Choose an explicit loop for logging, multiple accumulators, early exit, error evidence, or a rule that needs several readable steps.
12. Generator expressions avoid an intermediate collection
A generator expression uses parentheses and supplies values lazily:
any() can stop at the first truthy result, and no full Boolean list is needed. Similarly:
This is a preview, not a full generator lesson. Unit 5 explains iterators, generators, lazy state, and one-pass behavior. For now, recognize the common form when a consuming built-in needs values rather than a reusable list.
Checkpoint: readability and behavior
13. Clean an event log
Use this source log:
Build these artifacts with separate, readable comprehensions:
normalized_kinds: one lowercase stripped kind for every event;moves: the move values in source order, accepting kind spellings after normalization;unique_moves: a set of accepted move values;score_by_position: a dictionary mapping original positions to score values;latest_by_kind: a dictionary mapping normalized kind to its latest value;move_labels: strings such as"1: N"numbered from one; andhigh_scores: score values at least 5.
Run:
assert normalized_kinds == ["move", "noise", "move", "score", "score", "move"]
assert moves == ["N", "E", "N"]
assert unique_moves == {"N", "E"}
assert score_by_position == {3: 4, 4: 7}
assert latest_by_kind == {"move": "N", "noise": "?", "score": 7}
assert move_labels == ["1: N", "2: E", "3: N"]
assert high_scores == [7]
assert events[0]["kind"] == " move "Then expand any two comprehensions into loops and prove the results are equal. Explain why latest_by_kind loses earlier values and whether that is acceptable for its stated contract.
Hint: keep normalization beside each filter or output that needs it
Use event["kind"].strip().lower() as the normalized expression. Use enumerate(events) for original positions and enumerate(moves, start=1) for labels. A dictionary comprehension naturally applies “latest value wins” when normalized keys repeat.
Show one complete solution after attempting the lab
events = [
{"kind": " move ", "value": "N"},
{"kind": "noise", "value": "?"},
{"kind": "MOVE", "value": "E"},
{"kind": "score", "value": 4},
{"kind": "score", "value": 7},
{"kind": "move", "value": "N"},
]
normalized_kinds = [event["kind"].strip().lower() for event in events]
moves = [
event["value"]
for event in events
if event["kind"].strip().lower() == "move"
]
unique_moves = {move for move in moves}
score_by_position = {
position: event["value"]
for position, event in enumerate(events)
if event["kind"].strip().lower() == "score"
}
latest_by_kind = {
event["kind"].strip().lower(): event["value"]
for event in events
}
move_labels = [
f"{number}: {move}"
for number, move in enumerate(moves, start=1)
]
high_scores = [
event["value"]
for event in events
if event["kind"].strip().lower() == "score" and event["value"] >= 5
]The expressions are independent so each artifact has one clear job. Repeated normalized keys deliberately leave only the latest event in latest_by_kind.
14. Explain the compact forms
- How do you translate a comprehension into an explicit loop?
- Why does a trailing filter produce fewer outputs while a conditional expression produces one per input?
- What happens when transformed set values or dictionary keys collide?
- Why should code use the resulting collection rather than a target name after a comprehension?
- Which kinds of state changes or exits are clearer in an explicit loop?
Key points
TipKey points
- A comprehension builds one new list, set, or dictionary from source traversal.
- A trailing
iffilters inputs; a leading conditional expression chooses one of two outputs for every accepted input. - Sets remove equal outputs; repeated dictionary keys keep the latest assigned value rather than grouping automatically.
- Expand nested or surprising comprehensions into loops to verify evaluation order.
- Use comprehensions for readable collection construction, not side effects, several accumulators, or early exit.
- A generator expression can feed
any(),all(), orsum()without building an intermediate list; deeper lazy behavior belongs to Unit 5.