{
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
Producing Values One at a Time
Trace iterable and iterator state, build finite generators that pause and resume, and compose lazy pipelines whose consumers can stop early.
python-foundations
functions-call-behavior
iterators
generators
Course progress
0%
1. Sometimes the caller needs only the next useful reading
Suppose a remote sensor collected many values, but an alert needs only the first three readings above a threshold. Building every transformed result first can do unnecessary work. An iterator lets the consumer ask for one value at a time and stop early.
This lesson separates several related terms:
- an iterable can provide an iterator;
- an iterator remembers its current consumption position;
- a generator function defines paused production with
yield; - a generator object is the stateful iterator returned by calling that function; and
- a generator expression creates a compact lazy transformation.
The important question is not merely “is this lazy?” It is: which object owns the current position, when does code run, and what happens after exhaustion?
2. A list is iterable; iter creates an iterator
The list stores all three values and can create iterators. cursor is one iterator with a current position. Each next(cursor) returns the next item and advances that position.
A for loop performs this protocol for you: it gets an iterator, requests next values, and stops normally when the iterator signals that no value remains.
3. Exhaustion is persistent
After the third item, another plain next(cursor) raises StopIteration. Keep that failing call commented in the normal lesson run:
next also accepts a default result:
Exhaustion does not rewind the iterator. Once consumed, it stays consumed. A for loop catches the stop signal internally; ordinary application code rarely calls StopIteration handling directly.
4. Collections restart; iterator objects continue
Two loops over the list each start from a newly created iterator:
Two loops over the same iterator share its progress:
Create independent positions by calling iter on the collection twice:
The list is shared source data, but each iterator remembers its own position.
Checkpoint: iterator state
5. Calling a generator function does not run its body yet
A function containing yield is a generator function:
The assignment creates a generator object. It prints nothing yet. The first request begins execution:
At that point Python:
- creates local
start = 3for this generator call; - prints the start message;
- reaches
yield start; - produces
3; and - suspends the frame before
start -= 1.
The call and first execution are separate moments. This timing is one of the most important differences between an ordinary function call and a generator function call.
6. yield pauses without discarding local state
The next request resumes after the earlier yield:
Before yielding 2, the resumed body executes start -= 1, changing the remembered local value from 3 to 2. A later request repeats that sequence.
One generator object keeps its suspended frame between requests.
stateDiagram-v2 [*] --> Created Created --> Running: first next request Running --> Suspended: yield one value Suspended --> Running: next request resumes Running --> Suspended: yield another value Running --> Exhausted: return or body ends Exhausted --> [*]
A normal function loses its ordinary frame after returning. A generator frame is deliberately retained while suspended, including local values and the next instruction position.
7. return ends production; yield produces a stream item
Each yield contributes an item visible to ordinary iteration. Reaching return or the end signals exhaustion. Python generators can attach a value to StopIteration, but ordinary for and list(...) consumption does not treat that value as another yielded item. Foundations code should return only to stop unless a specialized protocol explicitly needs more.
Make finite boundaries clear. This course avoids an unbounded generator unless the consumer has an equally visible limit.
Checkpoint: generator timing
8. yield from delegates to another iterable
Without delegation, a generator can relay items with a nested loop:
yield from expresses the relay directly:
When a group is empty, delegation produces no values and continues. In the recursive leaf_names preview, yield from leaf_names(child) delegates to a child generator rather than materializing its entire result first.
9. Generator expressions are lazy comprehensions
Square brackets create a list immediately. Parentheses create a generator expression:
The partial next consumed the first lazy result. Converting the same generator to a list continues from its current position; it does not repeat 24.
Use a list when the complete result will be reused, indexed, or measured. Use an iterator when one-pass incremental consumption is the desired contract.
10. Consumers can stop a lazy source early
any and all short-circuit. They stop as soon as the answer is known:
The generator does not need to check 100 because 25 > 20 already makes any(...) true. Similar early stopping happens with next and bounded tools such as itertools.islice.
Lazy execution changes timing: errors and side effects inside production occur when a consumer requests the relevant value, not necessarily when the generator object is created. Prefer generators that yield data without surprising effects; the print above exists only to reveal timing.
11. Build a pipeline from small lazy stages
def above_threshold(values, threshold):
for value in values:
if value >= threshold:
yield value
def to_fahrenheit(celsius_values):
for value in celsius_values:
yield value * 9 / 5 + 32
source = [12, 18, 25, 30]
usable = above_threshold(source, 18)
converted = to_fahrenheit(usable)
assert next(converted) == 64.4
assert list(converted) == [77.0, 86.0]The first request flows through both stages only until one output is available. The pipeline resumes from its shared positions later. A stage should document that its input is consumed; passing the same iterator elsewhere means those consumers share progress.
13. Lab: stream usable sensor readings
Build a finite source and two lazy stages:
def sensor_readings(values):
"""Yield each supplied sensor value in order."""
raise NotImplementedError
def valid_readings(values, *, minimum=0, maximum=100):
"""Yield values inside the inclusive supported range."""
raise NotImplementedError
def changes(values):
"""Yield each difference from the previous value."""
raise NotImplementedErrorUse progressive consumption checks:
source_values = [-5, 10, 14, 200, 20, 20]
stream = sensor_readings(source_values)
assert iter(stream) is stream
assert next(stream) == -5
assert next(stream) == 10
assert list(stream) == [14, 200, 20, 20]
assert next(stream, "exhausted") == "exhausted"
fresh = sensor_readings(source_values)
usable = valid_readings(fresh, minimum=0, maximum=100)
assert next(usable) == 10
assert list(usable) == [14, 20, 20]
assert list(usable) == []
pipeline = changes(valid_readings(sensor_readings(source_values)))
assert next(pipeline) == 4
assert list(pipeline) == [6, 0]
assert source_values == [-5, 10, 14, 200, 20, 20]
assert list(valid_readings([], minimum=0, maximum=100)) == []changes needs one previous valid value before it can yield a difference. An empty or one-item input therefore yields no differences. Do not build hidden intermediate lists in these functions.
Hint: keep only the state each stage needs
The source yields each input directly. The validator yields only values inside both boundaries. In changes, obtain the first item with next(iterator, sentinel), then loop over the remaining items, yield current - previous, and update previous.
Show one complete solution after attempting the lab
def sensor_readings(values):
"""Yield each supplied sensor value in order."""
yield from values
def valid_readings(values, *, minimum=0, maximum=100):
"""Yield values inside the inclusive supported range."""
for value in values:
if minimum <= value <= maximum:
yield value
def changes(values):
"""Yield each difference from the previous value."""
iterator = iter(values)
missing = object()
previous = next(iterator, missing)
if previous is missing:
return
for current in iterator:
yield current - previous
previous = currentAfter all assertions pass, predict list(changes([7])) and list(changes([7, 3, 9])) before running them. Explain why the source list stays unchanged while iterator positions advance.
14. Explain the state owner
For each object in the lab, state:
- when its generator body first runs;
- which local values remain during suspension;
- which consumer requests the next value;
- whether it can restart; and
- what proves exhaustion.
Restart the notebook and run the full cell sequence once. Stateful examples are easy to misread when earlier exploratory calls already consumed values.
Key points
TipKey points
- An iterable can provide iterators; an iterator owns a one-pass consumption position.
- Collections commonly create fresh iterators, while reusing one iterator continues from its current position.
- Calling a generator function creates a generator object without running its body.
- The first request starts the body;
yieldproduces a value and preserves the suspended frame for a later request. - Reaching
returnor the end exhausts a generator permanently. - Generator expressions and pipelines perform work as consumers request it, so short-circuit consumers can stop early.
- Use a list for reusable materialized results and a generator when one-pass incremental production is the intended contract.