{
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
Sorting Collections Without Losing Meaning
Sort values and complete records by explicit policies while distinguishing mutation, new results, reversal, key functions, type boundaries, stability, and source preservation.
python-foundations
collections-iteration
sorting
ordering
Course progress
0%
1. Rank complete records, not disconnected fields
A tournament result connects a pilot, score, and completion time:
Sorting a separate score list would lose which pilot earned each score. The unit of movement must be the complete record. The sort policy then chooses which field determines order.
This lesson answers:
- Does the operation mutate the list or create a new list?
- Is reversal the same as sorting in descending order?
- How does a key function turn a domain rule into a comparable value?
- What happens when values are not mutually comparable?
- How does stable sorting preserve earlier order among equal keys?
2. sorted() creates a new list
The built-in sorted() accepts any iterable and returns a list:
scores remains in source order. ranked_scores is [74, 83, 91, 96].
The input does not need to be a list:
All three results are lists. Direct dictionary traversal supplies keys, so sorted(dictionary) sorts keys.
3. .sort() changes one list and returns None
The existing list changes and result is None. This makes a common bug clear:
The useful list name now refers to None. The sort already happened in place before assignment discarded the reference.
Choose from the state requirement:
- use
sorted(source)when source order remains evidence or both versions matter; - use
working.sort()when the working list itself should adopt the new order.
Copy before an intentional in-place sort
This separates preservation from mutation visibly.
4. Descending sort and reversal answer different questions
Use reverse=True to sort in descending order:
.reverse() merely flips the current list positions:
The result is not score-ranked; it is reverse arrival order.
reversed() supplies values in reverse traversal order without changing the source:
reversed(route) itself is a reverse iterator, not a list. Convert it only when the program needs to retain all reversed values as a list.
Checkpoint: mutation and return values
5. Strings follow Unicode ordering unless you choose another policy
Default string comparison is case-sensitive and based on Unicode code points:
Uppercase spellings can appear before lowercase ones. That behavior is deterministic but may not match a human-facing name policy.
The key argument asks Python to calculate a comparison key for each value:
str.casefold is passed without parentheses. Python calls it once per string for comparison and still returns the original spellings in the result.
This is a preview of a function used as a value. Unit 5 explains callables and callbacks fully. For now, read key=str.casefold as “compare the casefolded form of each original string.”
Other built-ins can express simple policies:
6. Mixed incomparable types should fail visibly
Python does not invent an ordering between unrelated types:
This raises TypeError because integer and string ordering is undefined. Do not silently sort everything by str unless lexicographic display text is genuinely the required policy:
The key strings are "3", "12", and "7", so this is not numeric order. A better data boundary often converts all inputs to one intentional type before storing them.
Missing values need an explicit policy too:
The error asks you to decide whether None belongs first, last, or outside the ranked data. Sorting cannot infer domain meaning.
7. Sort records by fields with itemgetter
operator.itemgetter() creates a key callable that retrieves dictionary fields:
from operator import itemgetter
results = [
{"pilot": "Nova", "score": 91, "seconds": 74},
{"pilot": "Mira", "score": 96, "seconds": 83},
{"pilot": "Sol", "score": 91, "seconds": 68},
]
by_score = sorted(results, key=itemgetter("score"), reverse=True)
for record in by_score:
print(record["pilot"], record["score"])Complete dictionaries move together; no pilot becomes disconnected from a score or time.
itemgetter() can retrieve several fields as a tuple key:
Python compares the score first and uses pilot only when scores are equal. The complete tuple key is ascending unless reverse=True reverses all of it.
An optional lambda spelling is common in Python code:
Read this as a tiny unnamed function that receives one record and returns its seconds. You do not need to write lambdas in this unit; itemgetter() and built-in functions cover the lessons. Unit 5 teaches function values and lambdas in context.
Checkpoint: key selection and type boundaries
8. Stable sorting preserves earlier decisions among ties
Python’s sort is stable: records with equal keys retain their relative order from the input.
from operator import itemgetter
results = [
{"pilot": "Nova", "score": 91, "arrival": 1},
{"pilot": "Mira", "score": 96, "arrival": 2},
{"pilot": "Sol", "score": 91, "arrival": 3},
{"pilot": "Ivo", "score": 91, "arrival": 4},
]
ranked = sorted(results, key=itemgetter("score"), reverse=True)
assert [record["pilot"] for record in ranked] == [
"Mira",
"Nova",
"Sol",
"Ivo",
]Nova, Sol, and Ivo all have score 91, so their arrival order remains intact. The list comprehension in this assertion is supplied only to inspect names; Unit 4 teaches comprehensions. An equivalent beginner-readable inspection is:
Use stable passes for mixed directions
Suppose higher scores rank first, but equal scores use fewer seconds first. One tuple key with reverse=True would reverse both score and seconds, making slower times win ties. Stable passes express the mixed directions:
results = [
{"pilot": "Nova", "score": 91, "seconds": 74},
{"pilot": "Mira", "score": 96, "seconds": 83},
{"pilot": "Sol", "score": 91, "seconds": 68},
{"pilot": "Ivo", "score": 91, "seconds": 74},
]
ranked = sorted(results, key=itemgetter("seconds"))
ranked = sorted(ranked, key=itemgetter("score"), reverse=True)
ranked_names = []
for record in ranked:
ranked_names.append(record["pilot"])
assert ranked_names == ["Mira", "Sol", "Nova", "Ivo"]Apply the less important rule first (seconds ascending), then the most important rule (score descending). Stability preserves time order inside equal-score groups, and Nova remains ahead of Ivo because both key fields tie and Nova appeared first.
Sort dictionary items, not just keys
Each item is (key, value), so itemgetter(1) selects the score. Equal scores retain dictionary insertion order. The source dictionary remains unchanged.
NoteDo not sort more often than the output requires
Sorting performs more work than one traversal. Sort once when the program needs an ordered view, retain the result while it remains valid, and avoid sorting the same unchanged collection inside another traversal. Unit 7 will give this comparison a formal vocabulary.
Checkpoint: stable record ordering
9. Produce the tournament leaderboard
Preserve the submitted result order and build two ranked views.
Requirements:
- create an
alphabeticallist by pilot name without case-sensitive surprises; - create a
rankedlist where higher score wins, fewer seconds breaks a score tie, and original submission order breaks a complete tie; - append one display line per ranked record with a one-based place; and
- leave the source list and every record unchanged.
assert [record["pilot"] for record in alphabetical] == [
"Ivo",
"Mira",
"Nova",
"Sol",
]
assert [record["pilot"] for record in ranked] == [
"Mira",
"Sol",
"Nova",
"Ivo",
]
assert leaderboard_lines == [
"1. Mira — 96 points — 83s",
"2. Sol — 91 points — 68s",
"3. Nova — 91 points — 74s",
"4. Ivo — 91 points — 74s",
]
assert source_results == [
{"pilot": "Nova", "score": 91, "seconds": 74},
{"pilot": "Mira", "score": 96, "seconds": 83},
{"pilot": "Sol", "score": 91, "seconds": 68},
{"pilot": "Ivo", "score": 91, "seconds": 74},
]Boundary variation: append {"pilot": "ada", "score": 96, "seconds": 83} to the source. Predict its alphabetical position and how the complete leaderboard tie uses submission order. Rerun from the source cell rather than mutating an already ranked result.
Hint: separate the alphabetic and competition policies
itemgetter("pilot") is case-sensitive, so use an optional supplied lambda for the alphabetical view: key=lambda record: record["pilot"].casefold(). For mixed competition directions, sort by seconds ascending first and then by score descending. Enumerate the ranked records from one for display.
Show one complete solution after attempting the leaderboard
alphabetical = sorted(
source_results,
key=lambda record: record["pilot"].casefold(),
)
ranked = sorted(source_results, key=itemgetter("seconds"))
ranked = sorted(ranked, key=itemgetter("score"), reverse=True)
leaderboard_lines = []
for place, record in enumerate(ranked, start=1):
line = (
f"{place}. {record['pilot']} — "
f"{record['score']} points — {record['seconds']}s"
)
leaderboard_lines.append(line)Both sorted() calls create outer lists and move references to complete records; they do not mutate the source records. The stable second pass preserves the seconds rule and original order where the full ranking key ties.
10. Explain the ordering policy
- Why is
items = items.sort()a bug even though the sort itself occurs? - When is reverse traversal different from descending sorting?
- What does a key callable return, and what values appear in the final list?
- Why should mixed types or missing values trigger a policy decision?
- How do stable passes express one descending and one ascending field?
Key points
TipKey points
sorted()returns a new list;.sort()mutates a list and returnsNone.reverse=Truereverses sort order;.reverse()andreversed()reverse current traversal order without calculating rank.- A key callable produces the comparison value while complete original items move into the result.
- Normalize or reject mixed data deliberately; sorting cannot invent domain rules.
- Python sorting is stable, so equal-key items retain their earlier relative order.
- Sort complete records to preserve relationships, and use stable passes for mixed ascending and descending field requirements.