{
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
Read MyPy Errors and Close Type Gaps
Run MyPy in strict mode, trace diagnostics through inference and narrowing, and repair real contract mismatches without broad ignores.
python-foundations
code-quality-maintainability
mypy
static-analysis
Course progress
0%
1. A diagnostic is a trail, not a verdict
Suppose the museum package declares this function:
A caller reads a text field but passes it without conversion:
Both snippets are valid Python syntax. Running the caller would eventually raise TypeError in subtraction. MyPy can compare the declared types without executing the call:
Before changing anything, answer these questions:
- Which file and line contains the incompatible call?
- What type did MyPy infer or read for the supplied value?
- What type does the function contract require?
- Is the annotation wrong, or did validation/conversion fail at the caller?
- Which focused command will prove that the repair closes this diagnostic?
Read the message from left to right:
| Part | Evidence |
|---|---|
src/midnight_museum/report.py |
File containing the reported use |
:4 |
Line where MyPy can demonstrate the mismatch |
error |
Severity |
Argument 1 ... |
Operation being checked |
str; expected int |
Actual and required static types |
[arg-type] |
Stable error-code family for documentation or a precise ignore |
The diagnostic line is not always the original source of the bad assumption. Here the boundary accepted text earlier. Repairing overtime_penalty to accept str would weaken a clear numeric contract. Validate or parse before the call:
Then rerun the smallest useful target and the project target:
2. Let inference do useful work, then inspect it deliberately
MyPy follows assignments and return paths even when every local has no explicit annotation:
It infers points_after_bonus as int from score + 25. If you are unsure what it knows, use reveal_type temporarily inside a static-only branch:
TYPE_CHECKING is false at ordinary runtime, so the undefined MyPy helper is not called. Remove the investigation after it answers the question; a project full of stale reveal_type notes becomes noise.
Inference is local evidence, not a reason to omit public contracts. Without parameter annotations, a function can become dynamically typed:
In permissive settings, score may become Any, and almost every operation is accepted. Strict mode reports the missing annotations and checks more of the body.
Checkpoint: read the diagnostic
3. Narrow a union along the same branches the runtime checks
MyPy rejects a string operation while winner might still be None:
Trying winner.upper() would produce a union diagnostic because None has no upper. Narrow it with the real runtime decision:
Inside else, the impossible None case has been removed. The same idea works with object and isinstance:
The second branch narrows value to int. The boolean check comes first because booleans are integer subclasses at runtime.
Exhaust every declared case
A small literal vocabulary can be checked with an explicit final assertion:
For this foundations lesson, the explicit branches are enough. Later Python and typing tools offer specialized exhaustive-check helpers, but do not introduce a new abstraction when the small vocabulary is already clear.
Each runtime branch removes impossible members from the static union.
flowchart TD
A[Value is str or None] --> B{Value is None?}
B -- Yes --> C[Handle absence]
B -- No --> D[Value is str]
D --> E[Use string operations]
4. Treat Any as a gap in the fence
Any is compatible with every type in both directions. That makes it useful at rare integration boundaries and dangerous as a default:
A checker permits the indexing, nonexistent method, attribute access, and int return claim because each value derived from Any is usually also Any. The code can fail anywhere at runtime.
object is safer when the boundary truly accepts any object:
With object, MyPy allows only operations valid for all objects until runtime checks narrow it. Use Any when interoperability genuinely requires opting out of static checking, not because a precise type takes another minute to design.
Find how Any entered
Common paths include:
- an unannotated function parameter or return value;
- a third-party library without type information;
- JSON decoded and immediately claimed to be a domain record;
- an overbroad
castor ignore; and - a library configured with
ignore_missing_importsfor every module.
Strict settings such as disallow_untyped_defs and warn_return_any help stop unchecked values from crossing public boundaries. Do not respond by annotating everything as Any; that satisfies syntax while removing the evidence you wanted.
Checkpoint: narrowing and Any
5. Put strict policy in the project
Command-line flags are easy to forget. Commit the shared contract in pyproject.toml:
strict = true enables a documented bundle of stricter checks. The bundle may evolve across major MyPy versions, which is one reason the development dependency and lock matter. The other warnings catch configuration or escape hatches that no longer serve a purpose.
Run the same target from the project root:
A clean result means MyPy found no issue covered by the configured checks. It does not mean inputs have been validated, branches have been tested, or the business rule is correct.
Adopt checking in manageable slices
For an older untyped project, turning on strict mode for everything can produce hundreds of interacting messages. Start with a coherent boundary:
- annotate a widely used public function;
- annotate the data entering and leaving it;
- remove the
Anygaps revealed nearby; - run a focused MyPy target;
- add that target to the quality gate; and
- expand without allowing new unchecked functions in the completed area.
A narrow configuration override can document a temporary boundary:
This is visible debt, not a finished solution. Prefer a module-scoped migration over global ignore_missing_imports = true, which can hide misspelled imports and missing type information everywhere.
6. Understand library types and precise ignores
A typed installed package may include inline annotations and a py.typed marker, or supply separate stub files ending in .pyi. A checker uses those records to understand imported functions. If no information is available, MyPy may report an import or treat values as Any, depending on configuration.
Do not silence every import problem first. Check:
- Is the import name spelled correctly?
- Is the dependency installed in the same environment as MyPy?
- Does the library ship types or recommend a stub package?
- Is a small local boundary wrapper easier to validate than allowing
Anythrough the project?
If an ignore is truly required, name the exact code and explain the external constraint:
A precise ignore can become stale. With warn_unused_ignores, MyPy reports it when the library later gains type information.
Avoid changing an expected return annotation merely to make a real implementation error disappear:
The right repair is to return text, not claim the public function returns str | int unless both types are genuine behavior:
Checkpoint: project policy and repair choices
7. Lab: close seven type gaps
Place this valid Python in src/midnight_museum/repairs.py, enable the strict configuration above, and run mypy src/midnight_museum/repairs.py:
from typing import Any
def first_team(teams):
if teams:
return teams[0]
def score_from_payload(payload: Any) -> int:
return payload["score"]
def render_score(score: int) -> str:
return score
def add_bonus(score: int, bonus: str) -> int:
return score + bonus
winner = first_team(["Moon Moths"])
print(winner.upper())Repair these seven gaps without weakening the intended behavior:
- annotate
teamswith the smallest interface needed for truth and position zero; - make
first_teamdeclare and return the absent case explicitly; - replace the
Anypayload withobjectand validate a dictionary integer field; - return text from
render_score; - give
bonusits actual numeric type; - narrow
winnerbefore calling.upper(); and - keep strict mode and finish with both a clean MyPy run and behavior assertions.
Hint A: start at the outer signatures
Use Sequence[str] for teams and str | None for the search result. Use object for the untrusted payload so every operation must be justified.
Hint B: narrow the payload in stages
First require dict; then use .get("score"); reject booleans and require an integer before returning the value.
Hint C: do not hide the winner case
Use an explicit if winner is None branch. The ordinary branch can safely call .upper() after the check.
Show a complete strict solution
from collections.abc import Sequence
def first_team(teams: Sequence[str]) -> str | None:
if not teams:
return None
return teams[0]
def score_from_payload(payload: object) -> int:
if not isinstance(payload, dict):
raise ValueError("payload must be a dictionary")
score = payload.get("score")
if isinstance(score, bool) or not isinstance(score, int):
raise ValueError("score must be an integer")
return score
def render_score(score: int) -> str:
return f"{score} points"
def add_bonus(score: int, bonus: int) -> int:
return score + bonus
winner = first_team(["Moon Moths"])
if winner is None:
winner_heading = "NO WINNER"
else:
winner_heading = winner.upper()
assert winner_heading == "MOON MOTHS"
assert score_from_payload({"score": 205}) == 205
assert render_score(205) == "205 points"
assert add_bonus(180, 25) == 205Run:
Do not accept the repair until static contracts and checked behavior both pass.
Key points
- Read a MyPy diagnostic as a trail from reported use to actual and expected types; the highlighted line may not be the original boundary mistake.
- Use inference for obvious locals and temporary
reveal_typeto inspect a confusing path. - Narrow unions and
objectwith the same runtime branches that make operations safe. Anydisables useful checking and tends to propagate; locate and contain its entry point.- Commit strict project policy, run a consistent target, and prefer precise, explained ignores over global silence.
- A clean MyPy run complements runtime validation and tests; it replaces neither.