{
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
Sets: Uniqueness and Group Comparisons
Use sets for unique membership and group comparisons, choose deliberate mutation methods, and represent immutable hashable groups with frozenset.
python-foundations
collections-iteration
sets
frozenset
Course progress
0%
1. Keep one copy of every discovered skill
An expedition log may repeat a skill whenever a crew member demonstrates it. A readiness check cares only whether each skill is available at least once:
The set has three members because equal duplicates collapse. A set models unique membership, not occurrence count or positional order. Preserve the original list when arrival order or duplicate evidence still matters.
Set literals use braces:
An empty set is the important exception:
{} already denotes an empty dictionary, so use set() when no members exist.
This lesson answers:
- Which operations mutate a set, and how do missing removals differ?
- How do union, intersection, difference, and symmetric difference answer distinct questions?
- What do subset, superset, and disjoint relationships prove?
- When does an immutable
frozensetfit where a mutable set cannot?
2. Membership matters; positions do not
Sets support len() and membership:
They do not support numeric indexing:
Python raises TypeError: 'set' object is not subscriptable. There is no promised “first” member for index zero to select.
Printing or traversing a set can show members in an order that looks stable in one session, but code must not treat it as a positional or display-order contract. Create an explicitly sorted list when presentation needs a predictable order:
Lesson 6 develops sorting policies. The result here is a list; the set remains unchanged.
NoteMembership is a natural set question
Sets are designed for repeated membership checks without scanning a positional sequence in the ordinary case. Keep a list as well if you need both original order and membership behavior. One collection does not need to satisfy every requirement.
3. Add one member or update from many values
.add() treats its argument as one member:
The set changes and the method returns None.
Adding an existing equal member has no effect on length:
.update() consumes members from one or more iterables:
A string supplies individual characters, just as it did for list.extend():
Use .add("NOVA") if the complete word should be one member.
4. Choose removal behavior from the missing-item rule
.remove() requires the member:
Removing an absent member raises KeyError:
.discard() makes absence a harmless no-op:
Use remove() when absence violates the task’s promise and discard() when “make sure it is absent” is the complete requirement.
.pop() removes and returns an arbitrary member:
Do not assert which token is removed. Set pop() does not mean “remove the last item.” Calling it on an empty set raises KeyError.
.clear() removes every member and returns None.
Checkpoint: creation and mutation
5. Set operations answer four different group questions
Suppose two crews have these skills:
Union: present in either group
Union combines membership and keeps one copy of overlap.
Intersection: present in both groups
The named spelling is nova_skills.intersection(mira_skills).
Difference: present only on the left
Difference is directional. Swapping operands changes the question.
Symmetric difference: present in exactly one group
The shared "repair" member is excluded. The named method is .symmetric_difference().
All four expressions above create new sets; neither source set changes. In-place forms such as |=, &=, -=, and ^= mutate the left set and should be used only when that state change is intended.
6. Compare readiness with subset relationships
An expedition is ready if all required skills are included in its available skills:
<=means subset, including equality;<means proper subset, requiring at least one additional member on the right;>=means superset, including equality; and>means proper superset.
Equality ignores insertion history and display order:
.isdisjoint() asks whether two groups share no members:
This expresses “no overlap” more directly than constructing an intersection only to compare it with an empty set.
Checkpoint: group operations and relationships
7. Set members must be hashable
Strings, numbers, and suitable tuples can be members:
A list cannot:
Python raises TypeError: unhashable type: 'list'. A mutable list could change in a way that invalidates the set’s membership organization. A tuple works only if its own relevant contents are hashable.
8. frozenset represents an immutable set value
Construct a frozen set from any iterable:
It supports non-mutating set operations and comparisons:
It has no .add(), .remove(), or .update() methods:
The resulting AttributeError confirms the immutable interface.
Because a frozenset is hashable when its members are hashable, it can itself be a set member or dictionary key:
Set equality ignores order, so the differently written key finds the same requirements group.
A set can contain frozen sets:
Choose frozenset because the value is conceptually an immutable membership group or must be hashable—not merely because its name sounds safer.
Checkpoint: hashability and frozenset
9. Solve the expedition-skills puzzle
Three explorers report skills with duplicates. Build stable evidence and answer the readiness questions without relying on set display order.
nova_log = ["mapping", "repair", "mapping", "translation"]
mira_log = ["navigation", "repair", "first aid", "repair"]
required = frozenset({"mapping", "repair", "translation"})
nova_skills = None
mira_skills = None
shared_skills = None
all_skills = None
nova_missing = None
mira_missing = None
nova_ready = None
mira_ready = None
team_signature = None
display_skills = NoneYour artifact must:
- convert each log to a unique skill set without changing either log;
- find shared and combined membership;
- find required skills missing from each explorer;
- calculate readiness with a subset relationship;
- create an immutable
team_signaturefrom the combined skills; and - create a predictable alphabetical list only for display.
assert nova_log == ["mapping", "repair", "mapping", "translation"]
assert mira_log == ["navigation", "repair", "first aid", "repair"]
assert nova_skills == {"mapping", "repair", "translation"}
assert mira_skills == {"navigation", "repair", "first aid"}
assert shared_skills == {"repair"}
assert all_skills == {
"mapping",
"repair",
"translation",
"navigation",
"first aid",
}
assert nova_missing == set()
assert mira_missing == {"mapping", "translation"}
assert nova_ready is True
assert mira_ready is False
assert type(team_signature) is frozenset
assert team_signature == all_skills
assert display_skills == [
"first aid",
"mapping",
"navigation",
"repair",
"translation",
]Boundary variation: add two more "repair" entries and one "navigation" entry to nova_log. Predict which assertions change before rerunning. The unique set should gain only navigation; duplicate repair records remain preserved in the log but not the set.
Hint: translate each sentence into one group question
Convert logs with set(...). Shared means intersection, combined means union, missing means required - available, and ready means required <= available. Pass the combined set to frozenset() and to sorted() for two different final representations.
Show one complete solution after attempting the puzzle
nova_log = ["mapping", "repair", "mapping", "translation"]
mira_log = ["navigation", "repair", "first aid", "repair"]
required = frozenset({"mapping", "repair", "translation"})
nova_skills = set(nova_log)
mira_skills = set(mira_log)
shared_skills = nova_skills & mira_skills
all_skills = nova_skills | mira_skills
nova_missing = required - nova_skills
mira_missing = required - mira_skills
nova_ready = required <= nova_skills
mira_ready = required <= mira_skills
team_signature = frozenset(all_skills)
display_skills = sorted(all_skills)The logs preserve order and duplicates as evidence. Sets answer membership questions. The frozen set is a stable value that could become a mapping key, and the sorted list exists only to guarantee display order.
10. Explain the group model
- Why does
set(log)lose information even when it correctly deduplicates? - When should missing removal raise
KeyError, and when is a no-op better? - Why is
a - bdifferent fromb - a? - What fact does
required <= availablestate? - Why can a
frozensetbe a dictionary key while an ordinary set cannot?
Key points
TipKey points
- Sets retain unique hashable members and do not promise numeric positions or a stable presentation order.
.add()adds one member;.update()consumes members from iterables..remove()requires a member,.discard()accepts absence, and.pop()removes an arbitrary member.- Union, intersection, difference, and symmetric difference answer distinct group questions without mutating their operands.
- Subset, superset, and disjoint comparisons state readiness and overlap rules.
frozensetprovides immutable set membership and can be hashable itself.- Keep an ordered source list when duplicates or arrival order remain meaningful.