{
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
Format and Lint a Project with Ruff
Configure Ruff, interpret rule diagnostics, review automatic fixes, and separate deterministic layout from human design and behavior evidence.
python-foundations
code-quality-maintainability
ruff
linting
formatting
Course progress
0%
1. Decide which question you are asking
A teammate sends this museum helper:
Before running a tool, ask:
- Is the layout consistent with the project?
- Does static source contain a suspicious or unused construct?
- Do the declared types agree across calls?
- Does the function return the correct data for real files and failures?
- Is the interface understandable to a future maintainer?
Those are different questions:
ruff formatrewrites deterministic layout.ruff checkreports configured lint rules and can fix selected findings.- MyPy compares static type contracts.
- pytest executes behavioral examples.
- a human decides whether
load_teamhas a useful name, contract, error policy, and responsibility.
Do not treat a clean result from one as a substitute for the others.
From the project root, run the linter without changing files:
For the helper above, Ruff 0.15.20 with the course project’s rules reports:
The file is valid Python and might even pass a happy-path test. The report gives a different form of evidence.
2. Read a Ruff finding before fixing it
Each diagnostic tells you:
| Field | Meaning in the sample |
|---|---|
I001 |
Rule code; I is the import-sorting family |
[*] |
Ruff marks an automatic fix as available |
| Message | The import block is not in configured order |
Path and 1:1 |
File, line, and column where the finding is anchored |
| Help | Proposed action, not proof that the action fits every intention |
F401 comes from the Pyflakes-compatible F family. It says the imported name is unused in this module. Usually deleting os is correct. Sometimes an import is deliberately re-exported from a package. In that case, make the public interface explicit—often through __all__—rather than accumulating unexplained ignores.
Run one rule or one file while investigating
Focus shortens the feedback loop:
The first selects a file, the second selects a rule for this run, and the third opens Ruff’s local explanation. Command-line selection is useful for diagnosis; the committed project configuration remains the shared policy.
Common rule families in this course
The repository selects a deliberate, moderate set:
| Prefix | Kind of evidence | Example |
|---|---|---|
E |
pycodestyle errors | ambiguous whitespace or a configured long line |
F |
Pyflakes correctness signals | undefined or unused names |
I |
import organization | standard-library and local imports out of order |
UP |
syntax compatible with the target Python | older typing syntax that can be modernized |
B |
bug-prone patterns | a mutable default or loop-variable capture |
RUF |
Ruff-specific checks | ambiguous or unsafe source patterns |
A rule family is not inherently right for every project. Select it because the team understands and wants its policy.
Checkpoint: classify Ruff evidence
3. Let the formatter own mechanical layout
This function is valid Python but inconsistent with the project:
Run the formatter in place:
The result is deterministic for the selected Ruff version and configuration:
To check without rewriting—appropriate for a gate—use:
A file needing changes produces a nonzero exit status. A clean run looks like:
Review formatting as a diff
Even a deterministic tool can touch many lines. Use Git to see the patch:
Check that only layout changed, then run behavior tests. A formatter aims to preserve behavior, but project confidence comes from the complete evidence chain, not trust in a slogan.
Do not fight the formatter line by line
Ruff’s formatter deliberately has a small configuration surface. Agree on a line length and a few stable preferences rather than hand-arranging every expression. If a formatted expression is difficult to read, improve the source structure—extract an intermediate value or function—instead of adding fragile spacing.
The names solve a reading problem that line wrapping cannot.
4. Apply fixes in an order you can explain
Ruff can apply marked safe fixes:
For the import sample, the first command removes os and sorts the remaining imports:
Run lint fixes before formatting because a fix may reorganize imports or source that then needs formatting. After both commands:
Safe does not mean semantically proven
A “safe fix” means Ruff classifies it as preserving runtime behavior for the rule’s model. It cannot know external reflection, unusual import side effects, or every dynamic dependency. Review the diff.
Ruff can expose fixes it labels unsafe, but do not enable them as a blanket shortcut:
This displays availability; adding --fix --unsafe-fixes would apply them. Use an unsafe fix only after reading the rule, understanding the behavior risk, and having checks that cover the affected contract.
Fix the cause or document a narrow exception
Suppose a pytest fixture intentionally receives an unused conventional argument. A per-file or rule-specific exception can be clearer than renaming values throughout production code. But an unexplained global ignore trains future maintainers to disregard useful output.
Use this decision order:
- Is the finding a real bug or unnecessary construct? Fix the source.
- Does a safe mechanical fix match the intention? Apply and review it.
- Is the rule wrong for the whole project? Change the explicit policy with a reason and review.
- Is one file genuinely different? Add the narrowest documented exception.
- Never change configuration merely to make a dashboard green.
Checkpoint: formatter and fixer workflow
5. Commit the policy in pyproject.toml
A shared project should not depend on one person’s editor settings. Put the policy beside other project metadata:
Understand each field
line-lengthguides wrapping and relevant lint rules; it is not permission to compress every statement up to character 88.target-versionlets Ruff avoid syntax unavailable to the oldest supported Python. It must agree with[project].requires-python.srchelps import classification for asrclayout.selectmakes the chosen rule families explicit. Ruff documentation advises adding families deliberately;ALLsilently grows when a new release adds rules.- formatter options choose a few stable preferences without attempting to encode personal layout for every construct.
Ruff discovers pyproject.toml, ruff.toml, or .ruff.toml by walking from a file toward parent directories. Run from the expected project root and inspect configuration when a result surprises you:
File discovery and Git ignores
By default, current Ruff versions discover Python files and Jupyter notebooks and respect common ignore files. Passing a file explicitly can override ordinary exclusion unless force-exclude is configured. This distinction explains why “Ruff skipped the generated directory during a project run” and “Ruff checked a named generated file” can both be true.
Use tool-specific exclusion when lint and format policy differ:
Avoid excluding tests merely because they reveal findings. Tests can have narrow, justified differences:
Only add a rule that is actually selected and understood. S101 is shown as a configuration-shape example; it is not selected by this unit’s base policy.
6. Diagnose configuration instead of guessing
Three common surprises have different causes:
“The formatter passed, but lint failed”
Expected. Formatting handles layout; lint rules inspect other source patterns. Run and interpret both.
“My editor and terminal disagree”
Check whether the editor uses the project configuration and the same Ruff version. An editor-specific override can take precedence. Run the committed command in the declared environment as the reproducible result.
“A rule appeared after an upgrade”
An explicit rule family can gain rules in a later tool release. Review the release and new diagnostic, then either repair, configure a reasoned exception, or pin/update the tool deliberately. Do not combine a large tool upgrade with an unrelated refactor if you want a reviewable diff.
Committed configuration and a resolved tool version produce the repeatable Ruff result.
flowchart LR A[Python files] --> D[Ruff run] B[pyproject policy] --> D C[Resolved Ruff version] --> D D --> E[Diagnostics] D --> F[Formatted diff] E --> G[Human review] F --> G
Checkpoint: configuration boundaries
7. Lab: turn six findings into a reviewed patch
Create src/midnight_museum/cleanup.py with this valid source:
Use the unit configuration, then:
- run
ruff checkwithout fixes and classify every finding by rule code; - predict which findings
--fixwill change; - apply safe fixes, then run
ruff format; - inspect the diff before editing remaining findings;
- replace the mutable default with
Noneand create a fresh list; - replace the explicit
== Truecomparison with the boolean condition; - add useful parameter/return annotations without turning this into the MyPy lesson again;
- run lint, format check, MyPy, and pytest; and
- explain one choice Ruff made and one design choice you made.
Hint A: start with imports
os is unused and the import block is out of order. Ruff can remove and sort those safely. Review the resulting import group.
Hint B: repair the mutable default
Use def labels(names: list[str] | None = None) -> list[str]:, then create result = [] if names is None else list(names). Returning a copy also avoids mutating a caller’s supplied list.
Hint C: finish with check-only commands
After edits, run ruff check src tests, ruff format --check src tests, mypy src, and pytest -q. The final gate should not rewrite anything.
Show one reviewed result
import json
from pathlib import Path
BONUS_POINTS = 25
def load_score(path: Path) -> int:
data = json.loads(path.read_text())
score = data["clues"] * 30
if data["bonus"]:
score += BONUS_POINTS
return score
def labels(names: list[str] | None = None) -> list[str]:
result = [] if names is None else list(names)
result.append("MIDNIGHT MUSEUM")
return resultThe automatic pass can remove/sort imports and normalize layout. A maintainer chooses the name BONUS_POINTS, the mutable-default repair, copy behavior, and boundary annotations. Add runtime JSON validation in the appropriate boundary lesson/project rather than claiming the decoded object is statically safe.
Key points
- Formatter, linter, type checker, tests, and human review answer different questions.
- Read path, position, rule code, message, and fix status before changing code.
- Apply lint fixes before formatting, inspect the diff, and rerun check-only commands.
- Safe fixes are still reviewed changes; unsafe fixes require explicit behavior reasoning.
- Commit a small understood policy and a compatible tool version instead of relying on editor state.
- A narrow justified exception is better than a global ignore, but repairing the source is usually the first choice.