Recommended resources
The freely available resources this lesson will direct you to.
Hello. In the previous lesson, you made the incident_api package safe and predictable to import: domain code depends inward on stable concepts, while executable behavior stays in explicit entry points. That structure becomes far more valuable when a tool checks it continuously rather than relying on memory or a later code review.
This lesson introduces Ruff as the project’s formatter and linter. By the end, your repository will have one checked-in policy for formatting and a deliberately small, useful rule set that catches common defects: unused imports, unsafe mutable defaults, import-order drift, and outdated syntax. You will also know which commands belong in local development versus CI.
Two complementary checks: formatting and linting
A formatter and a linter both improve consistency, but they answer different questions.
Formatting rewrites layout without judging the design of the code:
- indentation and blank lines;
- line wrapping;
- spaces around operators;
- quote style;
- formatting of collections, calls, and imports.
For example, a formatter may turn a cramped function call into several readable lines. There should be no team debate about such changes; everyone runs the same formatter.
Linting examines code for likely mistakes and enforceable conventions:
- an import that is never used;
- an undefined name;
- a
bare except; - a mutable list used as a function default;
- imports in an inconsistent order;
- syntax that can be modernized safely for the project’s supported Python version.
Ruff provides both capabilities. It is not a replacement for tests: a clean Ruff run cannot prove that an incident transition rule is correct. Nor is it a full static type checker; that is the focus of the next lesson. Think of Ruff as a fast, deterministic quality gate that catches a different class of problems before code reaches tests or review.
Watch the command-line workflow first. It demonstrates why checking, reviewing automatic changes, and formatting are distinct operations.
Python Tutorial: Ruff - A Fast Linter & Formatter to Replace Multiple Tools and Improve Code Quality
Watch Corey Schafer’s “Python Tutorial: Ruff” for a practical view of the command-line feedback loop. Focus on what Ruff reports, what it can safely fix, and why formatting is run separately.
Watch linting and fixes to see ruff check, safe automatic fixes, and a diff preview. Then watch formatting workflow for the formatter and the recommended order of operations.
For a professional project, the important point is not merely that each developer has Ruff installed. The same Ruff version, configuration, and commands must run locally and in CI. This is analogous to making an infrastructure policy executable and versioned, rather than depending on every operator’s local preferences.
Put the policy in pyproject.toml
Add Ruff as a development dependency using the dependency manager already used by the project, then regenerate and commit the lock file. For a project managed with uv, for example:
uv add --dev ruff
That places Ruff in the project environment rather than relying on whichever global version happens to be installed on a developer workstation or CI runner.
Ruff can read pyproject.toml, ruff.toml, or .ruff.toml. Your project already has a pyproject.toml, so keep the project metadata and tool policy together there. A root-level configuration also makes the repository’s settings the authoritative ones for developers and CI.
Read the official tutorial’s configuration guidance before copying the baseline below.
Read the “Configuration” and “Rule Selection” parts of Ruff’s official tutorial. They explain where Ruff discovers configuration, how pyproject.toml controls it, and why a focused rule selection is preferable to enabling every possible rule.
In “Configuration,” read from configuration discovery through the example that relates the supported Python version to Ruff’s target version. Then read all of “Rule Selection,” especially incremental selection. Notice the distinction between adding rules to an existing set and imposing a large policy all at once.
Here is a practical baseline for the incident API. Add it to the existing root pyproject.toml:
[tool.ruff]
line-length = 88
target-version = "py311"
[tool.ruff.lint]
select = [
"E4", # import-related pycodestyle errors
"E7", # statement-related pycodestyle errors
"E9", # runtime-error patterns detected statically
"F", # Pyflakes: unused imports, undefined names
"I", # import sorting
"B", # likely bugs and error-prone constructs
"UP", # syntax improvements compatible with target Python
]
Set target-version to the oldest Python version your application supports. The shown value is appropriate only if the project’s requires-python is compatible with Python 3.11. If the project supports Python 3.10, use "py310" instead. This matters because Ruff must not suggest syntax that production interpreters cannot run.
The select list is intentionally explicit. It replaces reliance on whatever Ruff’s evolving defaults happen to be and creates a compact team policy:
| Rule family | What it contributes to this project |
|---|---|
E4, E7, E9 |
Basic import, statement, and syntactic correctness checks |
F |
Dead imports and names that would fail at runtime |
I |
Stable, automatically sortable imports |
B |
Patterns that frequently hide bugs |
UP |
Modern Python syntax, limited by target-version |
Do not begin by selecting "ALL". A new codebase will often produce an overwhelming number of messages, some contradictory or inappropriate for an application project. A small policy that the whole team understands and follows is more valuable than a huge policy that gets ignored.
The Ruff configuration and test file image shows the same central idea in an existing FastAPI codebase: lint policy lives beside the rest of the project configuration and is applied to ordinary application and test code.

Read Ruff findings as design feedback
Consider this intentionally problematic helper:
from incident_api.incident_enums import IncidentSeverity
from typing import Optional
import os
def incident_labels(
extra_labels: list[str] = [],
prefix: Optional[str] = None,
) -> list[str]:
labels = ["incident"]
labels.extend(extra_labels)
if prefix is not None:
return [f"{prefix}:{label}" for label in labels]
return labels
With the configured rule families, Ruff can identify several issues:
osis imported but never used, reported byF401.- The imports are not in the expected order and grouping, reported by the
Ifamily. Optional[str]can becomestr | Nonewhen the configured Python version supports it, reported byUP.extra_labels=[]is a mutable default argument, reported byB006.
The mutable-default warning deserves particular attention. The list is created once, when Python defines the function, not every time the function is called. If the function ever mutates that list, state can leak from one request or test to another.
A corrected version is explicit about the absence of labels and creates fresh state within the function:
from incident_api.incident_enums import IncidentSeverity
def incident_labels(
extra_labels: list[str] | None = None,
prefix: str | None = None,
) -> list[str]:
labels = ["incident"]
if extra_labels is not None:
labels.extend(extra_labels)
if prefix is not None:
return [f"{prefix}:{label}" for label in labels]
return labels
The example still imports IncidentSeverity only to illustrate an unused project import; remove it too unless the function genuinely needs it. Ruff can remove many unused imports automatically, but it cannot decide whether the import represented unfinished work or a missing feature. Treat automated edits as proposed changes, then inspect them through Git before committing.
The previous lesson’s import guidance is reinforced here. Ruff can sort imports and flag unused ones, but it cannot fully judge architectural direction. A lint-clean circular dependency remains a circular dependency. Keep the one-way package structure as a design decision; use Ruff to eliminate routine noise around it.
Establish a safe local and CI workflow
Run Ruff from the repository root. If you use uv, prefix the commands with uv run; if you activate a virtual environment, invoke ruff directly.
During development, use this sequence:
ruff check --fix .
ruff format .
The first command applies Ruff’s safe lint fixes, such as removing an unused import or organizing imports. The second command applies the canonical layout. Afterward, inspect the resulting Git diff as you would inspect any other automated refactoring.
When you want to inspect proposed lint changes before files are modified, use:
ruff check --fix --diff .
For a clean verification without modifying working files:
ruff check .
ruff format --check .
These last two commands are the CI quality gate. ruff check . fails if lint violations remain. ruff format --check . fails if formatting would change a file. This prevents a pull request from passing tests while still adding inconsistent formatting or obvious static defects.
A small CI step might therefore run the following commands in order:
ruff check .
ruff format --check .
pytest
Keep Ruff before tests because it is quick and gives direct feedback. Tests should not be delayed by a typo, an unused import, or an import-order change that the developer can resolve immediately.
Avoid broad suppression
Eventually, a rule may be inappropriate in a narrowly defined context. Ruff supports line-level # noqa comments and file-level ignores, but a suppression should be exceptional and documented.
Use this decision process:
- Fix the code when the warning identifies a real defect or a harmless modernization.
- Change the shared policy only when the rule does not fit the project’s conventions at all.
- Suppress a single occurrence only when the code is intentional and the reason is clear.
For example, do not suppress an unused import merely to make Ruff quiet. Either remove it, or use the name in the code. Similarly, avoid creating broad test-directory exclusions before there is a demonstrated, justified need. Per-file ignores are useful boundaries, but they should not become a way to exempt tests from quality standards.
If you use VS Code, installing the official Ruff extension can provide formatting and diagnostics while editing. That is convenient, but it is not the enforcement mechanism. The checked-in configuration and CI commands remain the source of truth, so the same feedback appears for every contributor and on every build agent.
Implementation checkpoint
Apply the baseline to the incident project now:
- Add Ruff as a locked development dependency.
- Add the
[tool.ruff]and[tool.ruff.lint]configuration shown above, aligningtarget-versionwithproject.requires-python. - Run
ruff check --fix ., then inspect the diff rather than committing blindly. - Run
ruff format .. - Confirm the repository passes:
ruff check .
ruff format --check .
- Add those two verification commands to your CI pipeline’s quality stage.
If Ruff finds warnings in the domain modules from earlier lessons, resolve them rather than immediately adding ignores. In particular, check that import sorting has not concealed an import-direction problem and that any UP modernization remains compatible with the project’s minimum Python version.
Key takeaways
Ruff gives the project two related but separate safeguards:
ruff formatapplies deterministic code layout.ruff checkdetects likely defects and convention violations.ruff check --fixcan safely resolve many mechanical issues, but its changes still deserve review.- A compact explicit rule set—
E4,E7,E9,F,I,B, andUP—is a strong starting policy for this API. target-versionmust match the oldest Python version the deployed application supports.- CI should run
ruff check .andruff format --check .without modifying files. - Ruff complements tests and type checking; it does not replace either.
Next, you will add a dedicated static type checker and learn to correct genuine type errors without silencing useful warnings.