{
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
Lists: Keeping Values in Order
Build and maintain ordered Python lists while distinguishing positions, slices, mutation, method return values, aliases, and separate outer copies.
python-foundations
collections-iteration
lists
sequences
Course progress
0%
1. Pack an adventure in a deliberate order
An explorer’s backpack is more than a group of names. The first item is easiest to reach, the last was packed most recently, and two water bottles are genuinely two supplies. A list represents those facts:
Python creates one list containing four string references. Lists preserve insertion order, allow duplicates, and are mutable: their contents can change without binding the name to a different list.
An empty list can be written either way:
A list may legally mix types:
That flexibility is useful for an intentionally positional record, but most lists are easier to use when every item has the same role: a list of stop names, scores, or player records. Use a tuple or dictionary when each position has a different fixed meaning.
Questions this lesson will answer
- How can you read one item without confusing its position with its human count?
- Which operations change the same list, and which create a new one?
- Why does
append()sometimes produce an unexpected list inside a list? - What do removal methods return, and how do missing items fail?
- When is a copy separate, and where does a shallow copy stop?
2. Positions begin at zero
Python numbers sequence positions from zero:
The output is map, water, and torch. Human item number one is Python index zero. An index is an offset from the beginning, not an ordinal label.
Negative indexes count back from the end:
-1 selects the last item and -2 the item before it. This remains useful when the list length changes.
An absent position raises IndexError
The list has valid indexes 0 and 1. Asking for index 2 raises IndexError: list index out of range. Read the traceback together with len(short_route): the largest valid positive index is one less than the length.
Membership asks a different question:
Indexing asks “what is at this position?” Membership asks “does an equal value occur anywhere?” A list normally answers membership by scanning values until it finds a match or reaches the end.
3. Slices select a new list
A slice uses start:stop; the start is included and the stop is excluded:
The results are new outer lists. middle contains positions 1, 2, and 3, but not position 4. Omitting a bound means “from the beginning” or “through the end.”
A third slice part is the step:
The original route remains unchanged. A negative step visits positions in the opposite direction.
Unlike one-item indexing, an oversized slice is safe:
The first expression returns every available item; the second returns an empty list. Slices describe a range of available positions rather than demanding one exact position.
Checkpoint: positions and slices
4. Replace one item or an entire slice
Assigning through an index mutates the existing list:
Only position 1 changes. The list still has three items.
Slice assignment can replace, grow, or shrink a region:
The values on the right are spliced into the selected region. Because a list can change length, the replacement does not need the same number of items.
One surprising use inserts without removing anything:
The empty slice at position 1 becomes the insertion point.
5. Add one object or add its contents
The shape of the right-hand value determines which operation expresses the job.
append() adds exactly one object
The string becomes one new final item. If the argument is itself a list, that list remains one object:
The length is three; position 2 is the entire nested kit.
extend() adds values supplied by another iterable
Now the list is flat and has four strings. Be careful when extending with a string: strings supply characters one at a time.
This produces ['N', 'O', 'V', 'A']. Use append("OVA") if the complete word should be one item.
insert() chooses a position
The former position 1 and everything after it move right. Inserting repeatedly near the beginning requires shifting existing positions; frequent front insertion may suggest a different design later.
+ creates a new list
Neither input changes. Concatenation is useful when the program needs to preserve both sources and produce a third sequence.
6. Remove by value or by position
Python offers several removal operations because they answer different questions.
remove(value) deletes the first equal value and returns None. pop() removes and returns the final item by default.
Choose a position for pop(index):
Use del when no removed value is needed:
clear() removes every item but keeps the same list object:
The result is None. Methods that mutate a list generally communicate their effect through the changed list, not by returning that list.
Missing removals expose different boundaries
This raises ValueError because the requested equal value is absent.
This raises IndexError because the position is absent. Decide whether the task identifies an item by equality or by position before choosing a method.
7. Search and count duplicates deliberately
index() returns the first matching position:
The outputs are 1, 2, and 0. count() has a natural zero result for an absent value; index() raises ValueError when no match exists.
You can constrain the index() search with start and stop positions:
This returns index 3 because the search begins at position 2. It still finds only the first match within the chosen range.
NoteOperation cost as a practical choice
Reading items[5] uses a known position directly. Checking target in items, calling index(), count(), or remove() may inspect many items. That is often fine for a short sequence. If a program repeatedly looks up thousands of values by stable IDs, Lesson 3’s dictionary may fit the question better.
Checkpoint: mutation and return values
8. An alias is not a backup
Assignment does not copy a list:
Both names show the added torch because both names reach the same list object.
The diagram distinguishes a second name for the same list from a separate outer list copy.
flowchart LR
A["backpack name"] --> B["one list object"]
C["backup name"] --> B
D["snapshot name"] --> E["separate outer list"]
Create a separate outer list with .copy(), list(...), or a full slice:
Equality tells you the values currently compare the same; changing one outer list proves whether its top-level structure is separate.
An outer copy does not duplicate nested values
Both displays contain the torch inside the first nested list. The outer lists are separate, but their positions still refer to the same nested list objects. This is a shallow copy. Unit 6 develops the complete identity and nested-copy model; for now, state whether your task needs only a separate outer sequence or fully independent nested state.
9. Visit items without changing the list
A straightforward for loop receives list values in order:
The target name stop is rebound for each iteration. Assigning a new value to that name does not replace the list position:
items is unchanged. You would need to assign through a position or build a new list to preserve transformed strings. Lesson 5 examines positions with enumerate() and the risks of mutating while traversing. Unit 4 develops filtering and accumulation patterns.
10. Build the explorer backpack
Complete this lab without opening the support first. The source manifest is evidence and must remain unchanged.
Your program must:
- create a separate outer working list;
- replace the second item with
"canteen"; - insert
"compass"immediately after"map"; - remove and preserve the rope by its position;
- add both emergency-kit values as separate items;
- add
"sealed rations"as one final item; - derive the first item, final two items, and original water count; and
- preserve
source_manifestandemergency_kitexactly.
Run these checks:
assert source_manifest == ["map", "water", "rope", "water", "torch"]
assert emergency_kit == ["bandage", "whistle"]
assert working_pack is not source_manifest
assert removed_item == "rope"
assert first_item == "map"
assert water_count == 2
assert last_two == ["whistle", "sealed rations"]
assert final_pack == [
"map",
"compass",
"canteen",
"water",
"torch",
"bandage",
"whistle",
"sealed rations",
]
assert working_pack == final_packThen modify the source so it begins with two maps and ends with no torch. Update only the assertions whose promised facts genuinely change.
Hint: choose an operation from the shape of each requirement
Use .copy() before mutation. Assignment through index replaces one item; .insert() chooses a position; .pop(index) preserves the removed item; .extend() adds the kit’s contents; and .append() adds the final string as one item. Calculate facts from the appropriate source before changing it.
Show one complete solution after attempting the lab
source_manifest = ["map", "water", "rope", "water", "torch"]
emergency_kit = ["bandage", "whistle"]
first_item = source_manifest[0]
water_count = source_manifest.count("water")
working_pack = source_manifest.copy()
working_pack[1] = "canteen"
working_pack.insert(1, "compass")
removed_item = working_pack.pop(3)
working_pack.extend(emergency_kit)
working_pack.append("sealed rations")
last_two = working_pack[-2:]
final_pack = working_pack.copy()The solution takes facts about the untouched manifest before mutation. The final copy is not required for the mechanics, but it makes the delivered artifact explicit and keeps later edits to working_pack from changing its outer list.
Checkpoint: aliases and copy shape
11. Explain the choices, not only the result
Before leaving the lesson, answer in your own words:
- Why does indexing beyond the end fail while an oversized slice succeeds?
- When should another list be passed to
append()rather thanextend()? - Which removal operation should you use when the removed value is needed?
- Why does assigning the result of
append()destroy the useful list name? - What independence does a shallow list copy guarantee, and what does it not guarantee?
Key points
TipKey points
- Lists preserve order, allow duplicates, and can change length and contents.
- Indexing selects one exact position; slicing returns a new outer list over an available range.
append()adds one object,extend()adds supplied items,insert()chooses a position, and+produces a new list.remove()deletes the first equal value;pop()removes and returns by position;delremoves without returning the item.- In-place methods such as
append(),extend(),remove(), andclear()returnNone. - Assignment can create an alias.
.copy(),list(...), and[:]create a separate outer list but do not recursively copy nested objects.