{
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
Designing Clear Parameters and Calls
Predict how Python binds positional, keyword, default, keyword-only, variadic, and unpacked arguments while designing interfaces that remain readable across repeated calls.
python-foundations
functions-call-behavior
parameters
interfaces
Course progress
0%
1. Make configurable calls readable
This call is legal but difficult to interpret:
Which 1 controls precision? What does True mean? A clearer interface separates essential data from configuration:
name and distance may be supplied by position. Parameters after * are keyword-only, so their meaning appears at every call.
Questions this lesson will answer
- When does Python evaluate arguments, and how are they bound?
- Which call shapes raise
TypeErrorbefore the body starts? - Why can a mutable default remember earlier calls?
- When do
*args,**kwargs,*sequence, and**mappinghelp? - What do docstrings and annotations promise—and what do they not enforce?
2. Python evaluates arguments before the body begins
The output order is:
Python evaluates the argument expression prepare() first. Only after it returns "map" can Python bind local parameter value and begin announce.
Multiple argument expressions are evaluated left to right:
Do not put surprising state changes inside arguments. The evaluation order is predictable, but explicit intermediate names are often clearer.
3. Positional arguments bind by order
The first argument binds to name; the second binds to distance. Reversing them does not produce a binding error—it produces a nonsensical message:
Python binds by position, not intended meaning. Useful parameter names, annotations, and keyword calls help humans detect the mistake.
4. Keyword arguments bind by parameter name
Keyword arguments can appear in a different order because their labels determine binding. Positional arguments must come before ordinary keyword arguments in a call:
A value cannot bind the same parameter twice:
If uncommented, Python raises TypeError because positional "Ridge" already bound name, then name="Cave" tries again. Binding fails before the body runs.
Checkpoint: argument binding
5. Call-shape mistakes fail before body execution
For this definition:
These commented calls each have a different binding problem:
If run separately, they raise TypeError for:
- a missing required
distanceargument; - too many positional arguments; and
- an unexpected
mileskeyword.
Read the function name and parameter names in the message. Unit 8 develops a full traceback method; here the immediate fact is that no valid local parameter frame could be constructed.
6. Defaults make an input optional for the caller
The default is used only when the call supplies no value for unit. Required ordinary parameters must appear before defaulted ones in the definition.
Defaults are part of the public contract. Changing unit="km" to "miles" changes existing calls that omitted that argument even though their code has not changed.
7. A mutable default remembers earlier calls
Default expressions run once when Python executes the definition:
Both calls use the same default list. This may surprise callers expecting fresh per-call storage.
Use None as a sentinel and create the list inside each call:
An explicitly supplied list is still deliberately changed:
Unit 6 develops identity and mutation in depth. The interface lesson’s rule is: never use a mutable default as accidental per-call state.
8. Keyword-only parameters make configuration explicit
A call such as format_distance(12.345, 2) raises TypeError; precision must be named. This is useful when optional values share types or when the call should read like configuration.
Avoid “Boolean soup”:
report("ridge", uppercase=True, include_count=False) communicates far more than report("ridge", True, False).
Checkpoint: defaults and keyword-only inputs
9. Read positional-only markers in documentation
Some built-ins display / in their signature. A small user-defined example is:
Parameters before / are positional-only. percentage(part=1, whole=4) would raise TypeError. Parameters after * are keyword-only. The ordinary parameters between those markers, if any, may be supplied either way.
You do not need to make every interface positional-only. Learn to read the marker because it appears in Python’s documentation and permits API designers to keep some parameter names from becoming caller commitments.
10. *args collects extra positional arguments
Inside the function, distances is a tuple. This interface is honest when any number of same-role values makes sense. It would be worse for a fixed record such as (name, distance, unit), whose roles deserve explicit names.
Ordinary parameters can come first:
11. **kwargs collects extra keyword arguments
Inside, tags is a dictionary. Use this only when a genuinely open set of named options is part of the contract or when forwarding another interface. Do not replace three known parameters with **kwargs; that hides spelling errors and removes useful signature guidance.
12. Stars at a call unpack existing collections
A star in a call has a different role from a star in a definition:
*coordinate supplies its items as positional arguments.
Double-star supplies mapping entries as keyword arguments:
Keys must match accepted keyword names. A duplicate still fails:
Here both the explicit keyword and mapping try to bind precision.
13. Forward arguments without changing their call shape
A small forwarding wrapper can accept and pass through another function’s call:
Lesson 8 uses this shape in decorators. Forwarding is a legitimate variadic use because the wrapper supports the wrapped callable’s argument interface rather than pretending its own domain has unknown fields.
Checkpoint: variadic and unpacked calls
14. Docstrings and annotations communicate; they do not coerce
The docstring explains purpose. Annotations describe intended value roles to readers and tools. Python still permits a call such as repeat_message("ha", "3") to reach the body; annotations do not convert the string or automatically reject it. That operation then raises TypeError.
Keep the lesson boundary clear:
- Unit 5 uses annotations to make a signature legible.
- Unit 14 develops annotation design and static checking.
- Unit 15 develops docstring conventions and API documentation.
You can inspect an interface:
15. Build a mission-message interface
Complete this scaffold:
Contract:
nameanddistanceare the essential positional-or-keyword data.- All configuration is keyword-only.
notes=Nonecreates fresh per-call notes storage; an explicitly supplied list is read but not changed.- Ignore empty note strings and join non-empty notes with
"; "after the base message. - Prefix
"URGENT: "only when requested. - Format distance with the requested precision and unit.
- Return the string without printing.
Run:
assert build_mission_message("Moon Pass", 12.345) == "Moon Pass: 12.3 km"
assert build_mission_message(
"Moon Pass", 12.345, precision=2, unit="miles", urgent=True
) == "URGENT: Moon Pass: 12.35 miles"
assert build_mission_message(
"Ridge", 7, notes=["windy", "", "bring rope"]
) == "Ridge: 7.0 km | windy; bring rope"
assert build_mission_message("Dock", 0, notes=[]) == "Dock: 0.0 km"
notes = ["night route"]
result = build_mission_message("Cave", 3, notes=notes)
assert notes == ["night route"]
assert result.endswith(" | night route")Then call it with:
Use both unpacking operators and prove the result is "URGENT: Garden: 6 km".
Hint: separate binding from message assembly
Treat notes is None as no notes for this call. Build a list of non-empty note strings without changing the supplied collection. Assemble the base string, optional urgent prefix, and optional notes suffix in named stages.
Show one complete solution after attempting the lab
def build_mission_message(
name: str,
distance: float,
*,
precision: int = 1,
unit: str = "km",
urgent: bool = False,
notes=None,
) -> str:
"""Return one formatted mission message."""
if notes is None:
notes = []
kept_notes = []
for note in notes:
if note:
kept_notes.append(note)
prefix = "URGENT: " if urgent else ""
message = f"{prefix}{name}: {distance:.{precision}f} {unit}"
if kept_notes:
message += " | " + "; ".join(kept_notes)
return message
mission_data = ("Garden", 5.5)
mission_options = {"precision": 0, "urgent": True}
unpacked_message = build_mission_message(*mission_data, **mission_options)The function does not mutate an explicit notes list. The None sentinel still makes the absence of notes distinct from a shared mutable default.
16. Explain the interface
- When are argument expressions evaluated relative to the body?
- Which calls bind by order, and which bind by parameter name?
- Why is a mutable default shared, and how does
Nonechange the timing? - How do
*and**differ in definitions and calls? - What do annotations communicate without enforcing at runtime?
Key points
TipKey points
- Python evaluates arguments, binds parameters, and only then begins the body.
- Positional values bind by order; keyword values bind by name; duplicate, missing, extra, or unexpected bindings raise
TypeError. - Defaults are evaluated at definition time. Use a sentinel for fresh mutable per-call state.
- Keyword-only parameters make configuration visible; positional-only markers commonly appear in built-in documentation.
*argsand**kwargscollect variable arguments in definitions;*iterableand**mappingexpand arguments at calls.- Docstrings and annotations communicate the interface but do not perform runtime conversion or validation.