{
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
Tuples and Unpacking Fixed Records
Represent fixed positional records with tuples, unpack their values into meaningful names, and diagnose comma, immutability, length, and starred-shape boundaries.
python-foundations
collections-iteration
tuples
unpacking
Course progress
0%
1. Give each clue position one fixed meaning
A treasure scanner produces a row, a column, and a symbol together:
This tuple is an ordered three-position record. Position 0 always means row, position 1 column, and position 2 symbol. Unlike an adventure backpack, the clue is not intended to gain or lose positions.
Tuples work well for small values whose length and positional meaning are fixed: coordinates, RGB colors, database result rows, or the pair returned by divmod(). A dictionary is usually clearer when a record has many optional or self-describing fields.
This lesson answers four practical questions:
- What syntax creates zero-, one-, and many-item tuples?
- Which sequence operations still work when positions cannot be changed?
- How does unpacking make positional meaning visible through names?
- When should a variable-length remainder use one starred target?
2. The comma creates the tuple
Parentheses make tuple boundaries easy to see, but the comma is the decisive syntax:
This difference matters for a one-item tuple:
(5) merely groups the integer expression. (5,) is a tuple containing one integer. A trailing comma is also accepted in longer tuples and makes multi-line editing safer:
The empty tuple needs no comma:
The tuple() constructor consumes values from another iterable:
The string supplies individual characters; the list supplies its integer items.
3. Read a tuple like any ordered sequence
Tuples support length, indexing, negative indexing, slicing, membership, counting, and first-match search:
clue[1:3] is a new tuple because slicing preserves the sequence family. The count is two, and index("N") returns only the first matching position, index 2.
As with a list, an absent numeric position raises IndexError and a missing value passed to .index() raises ValueError.
Tuple positions cannot be replaced
Python raises TypeError: 'tuple' object does not support item assignment. Tuples also have no .append(), .remove(), or .clear() methods. To represent a different coordinate, create a different tuple:
The original remains (12, 7).
An immutable tuple can point to a mutable value
The tuple still has the same two positions and still refers to the same list in position 1. The nested list itself permits mutation. “A tuple is immutable” means its own sequence of references cannot be replaced, added, or removed; it does not promise that every reachable object is frozen. Unit 6 explores that object graph in depth.
Checkpoint: tuple syntax and immutability
4. Unpack fixed positions into meaningful names
Unpacking assigns each supplied position to a target name:
Read the assignment in two stages. Python evaluates the right side first and obtains three values. It then assigns the first to row, the second to column, and the third to symbol.
The names expose meaning that clue[0], clue[1], and clue[2] hide:
Parentheses around targets are optional:
Use them when they clarify a nested record boundary, not because unpacking requires them.
Target count must match value count
Python raises ValueError: too many values to unpack (expected 2).
This raises ValueError: not enough values to unpack. These errors are evidence that the supplied record shape disagrees with the assignment’s promised shape. Do not silence them by adding meaningless throwaway names until you understand which data contract changed.
Swap names without a temporary variable
Python first packs the right-side values, then unpacks them into the left-side names. left becomes "key" and right becomes "map" without losing either original value.
Unpack values returned together
divmod() returns quotient and remainder as a two-item tuple:
Naming the two meanings immediately is clearer than retaining a value such as result[0] and result[1].
5. One starred target absorbs a variable-length remainder
When the first and last positions have fixed meanings but the middle varies, use one starred target:
first is "N", last is "A", and middle is the list ["O", "V"]. The starred target always collects its share into a list, even when the source is a tuple.
It can appear at the beginning or end:
One assignment may contain only one starred target because two stars would make the division ambiguous:
The syntax is invalid before the program runs.
An empty remainder is valid:
There are still enough values for the two unstarred targets.
Checkpoint: ordinary and starred unpacking
6. Choose tuple or list from the promised lifecycle
Both types preserve order, allow duplicates, and support indexes and slices. The difference is what the program promises to do next:
| Requirement | Prefer | Reason |
|---|---|---|
| Ordered collection that grows or shrinks | list |
Its sequence structure is mutable. |
| Small fixed positional record | tuple |
Its length and positions cannot be changed. |
| Named record fields | dict |
Keys communicate meaning better than distant numeric positions. |
| Unique membership with no positions | set |
Uniqueness and group comparisons are its purpose. |
Do not choose a tuple merely to prevent every possible change. A tuple containing mutable objects is not deeply frozen, and a value often needs validation beyond its container type. Choose it when fixed positional shape communicates the model.
Hashability is a practical preview
Some tuples can be dictionary keys or set members:
A tuple is hashable only when all values needed for equality are themselves hashable. A tuple containing a list is not:
This raises TypeError: unhashable type: 'list'. Lessons 3 and 4 use the rule; Unit 6 explains hashability systematically.
7. Decode the treasure-map clues
The scanner supplies fixed clue records. Preserve the source tuple and derive named facts and a final word.
Complete these stages:
- unpack the first clue and collect all remaining clues with a starred target;
- unpack the first clue into its three meaningful names;
- unpack the second clue from
remaining_clues; - create a tuple containing the four symbol positions; and
- join those symbols into the vault word.
Use these progressive checks:
assert source_clues[0] == (12, 7, "N")
assert source_clues[-1] == (1, 5, "A")
assert first_clue == (12, 7, "N")
assert remaining_clues == [
(4, 11, "O"),
(9, 2, "V"),
(1, 5, "A"),
]
assert (first_row, first_column, first_symbol) == (12, 7, "N")
assert (second_row, second_column, second_symbol) == (4, 11, "O")
assert letters == ("N", "O", "V", "A")
assert vault_word == "NOVA"
assert source_clues == (
(12, 7, "N"),
(4, 11, "O"),
(9, 2, "V"),
(1, 5, "A"),
)For a boundary test, temporarily change one clue to (4, 11) and run the line that unpacks it into three names. Record the error message, restore the source, and explain why the failure is useful contract evidence.
Hint: reveal fixed meanings before assembling the word
Use first_clue, *remaining_clues = source_clues. Ordinary three-name unpacking then exposes each clue. The final tuple can select index 2 from each of the four known fixed records, after which "".join(letters) produces text.
Show one complete solution after attempting the lab
source_clues = (
(12, 7, "N"),
(4, 11, "O"),
(9, 2, "V"),
(1, 5, "A"),
)
first_clue, *remaining_clues = source_clues
first_row, first_column, first_symbol = first_clue
second_row, second_column, second_symbol = remaining_clues[0]
letters = (
first_symbol,
second_symbol,
source_clues[2][2],
source_clues[3][2],
)
vault_word = "".join(letters)The final two numeric chains are acceptable here because every clue has already been established as the same compact three-position shape. In a larger or less stable record, named dictionary fields would communicate the access better.
Checkpoint: choosing and using fixed records
8. Explain the record contract
Answer these before moving to dictionaries:
- Why does
(5)produce an integer while(5,)produces a tuple? - What part of
("NOVA", ["map"])is protected from change, and what part is not? - Why can unpacking be safer to read than repeated numeric indexing?
- What type does a starred target produce, and why might that matter later?
- When would named dictionary fields communicate a record better than tuple positions?
Key points
TipKey points
- The comma creates a tuple; parentheses normally clarify its boundary.
- Tuples preserve order and duplicates but do not permit replacing, inserting, or deleting their own positions.
- A tuple may still contain a mutable object.
- Ordinary unpacking requires matching value and target counts; one starred target absorbs a variable-length remainder into a list.
- Fixed, compact positional records often suit tuples; changing sequences suit lists, and named records often suit dictionaries.
- A tuple can be a dictionary key or set member only when its relevant contents are hashable.