Ruff combines fast linting with Python formatting. The linter finds unused imports, likely mistakes, and selected rule violations; the formatter controls presentation. These are complementary jobs, not replacements for tests or code review.
Start with a small rule set the team can maintain. The goal is useful feedback, not hundreds of ignored warnings.
Install and configure
python -m pip install ruff
ruff check .
ruff format --check .
Add a conservative configuration to pyproject.toml:
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "B"]
F enables Pyflakes checks, I handles imports, and B adds bugbear warnings. Read the official configuration reference before enabling broad rule families.
To apply changes:
ruff check --fix .
ruff format .
The formatter does not sort imports by itself, so run lint fixes first. The formatter documentation lists lint rules that conflict with formatting.
Adopt Ruff without noisy history
Run ruff check without fixes first. Address objective errors, commit configuration, and expand rules later. Avoid mixing a repository-wide format with functional changes because it makes review and history harder.
CI should validate without rewriting files:
ruff check .
ruff format --check .
Pair these checks with pytest tests. A linter cannot prove business behavior, and a formatter cannot detect a broken API contract.
- Set
target-versionto the Python versions actually supported. - Keep editor, terminal, hooks, and CI on the same configuration.
- Review automatic fixes and run tests.
- Document intentional ignores next to the narrowest scope possible.
A short, mandatory configuration creates more value than an ambitious policy everyone bypasses. Ruff works best when its output is quick, consistent, and actionable.
Understand the boundary between linting and formatting
ruff check analyzes source and reports selected rule violations, such as an undefined name, unused import, suspicious comparison, or incorrectly ordered import. ruff format rewrites presentation choices such as whitespace and line wrapping. It does not attempt to fix every diagnostic or validate business logic.
That boundary determines command order. With the I family enabled, ruff check --fix sorts imports, then ruff format stabilizes layout. To validate without changing files, use ruff check . and ruff format --check .; these are the right forms for CI.
A clean Ruff run is not proof that an application works. Tests exercise behavior, while a type checker analyzes type contracts. Each tool covers a different failure class, so replacing the test suite with linting would create false confidence.
Choose rules for this repository
Rule codes are grouped by origin and purpose. E4, E7, and E9 enable selected pycodestyle errors; F covers Pyflakes; I handles import ordering; and B brings in flake8-bugbear checks. The official rule reference explains every diagnostic and its fix availability.
Do not copy a maximal rule list from an unrelated project. A web service, public library, teaching repository, and notebook collection have different constraints. Start with high-confidence errors and add families in reviewable changes. A rule is valuable when the team understands and consistently enforces it.
Prefer narrow exceptions to a global ignore. Tests may deliberately use assert, for example:
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"]
If the security family is enabled, that exception documents why test files differ. For one intentional line, # noqa: CODE is more precise than a bare # noqa, which can hide unrelated diagnostics added later.
Handle generated files and notebooks explicitly
Virtual environments, caches, and build output should not be analyzed. Ruff recognizes common exclusions, while extend-exclude can add project-specific paths. Avoid excluding a source directory merely to make CI green; if it contains maintained code, record a staged cleanup instead.
Generated code needs a policy. If another tool always recreates it, reformatting may produce noise that disappears on the next generation. Exclude it or configure the generator. If generated files become maintained source, apply the normal checks.
Ruff can process notebooks, but cell order changes the meaning of some diagnostics. Test fixes on a copy and decide whether notebooks belong in scope. Running at the repository root should not silently rewrite educational or exploratory material the team never intended to format.
Review automatic fixes
--fix applies available fixes under Ruff's safety policy. Some transformations are classified as unsafe because they may alter runtime behavior or remove useful information. Do not enable --unsafe-fixes as a blanket shortcut; inspect the diagnostic and proposed change first.
Safe fixes still deserve a diff review. An apparently unused import might exist for a side effect, which should be made explicit rather than silently preserved. Import reordering can expose a circular dependency that relied on accidental order. Run tests after broad fixes.
ruff check --diff is useful when you want to inspect proposed fixes without touching files. For a large adoption, enable and fix one family at a time. Keep the formatting-only commit separate from functional work so review, blame history, and rollback remain practical.
Align Python version and line length
target-version tells Ruff which syntax and modernization suggestions are valid. It should agree with requires-python and the CI matrix. Setting py312 while promising Python 3.10 can produce advice that older interpreters cannot run.
line-length guides both linting and formatting, but it is not an absolute guarantee. Long URLs, strings, and constructs that cannot be split may remain over the limit. Avoid adding formatting-conflicting lint rules just to force every line under a visual threshold.
When replacing Black, run a full comparison on a dedicated branch. Ruff aims for a compatible style, but documented differences exist. Once the migration is accepted, remove the previous formatter command, editor integration, and stale configuration. Two formatters competing over the same files create alternating diffs.
Editors, hooks, and CI
Editor integration can format on save and display diagnostics, but committed configuration remains the source of truth. Contributors may choose when to apply fixes as long as the final checkout passes identical commands.
Local hooks shorten feedback, yet they cannot replace CI because hooks can be skipped and local installations drift. If the project uses pre-commit hooks, pin the integration revision and keep its arguments aligned with documented terminal commands.
CI should inspect rather than rewrite:
ruff check --output-format=github .
ruff format --check --diff .
python -m pytest
Provider-specific output improves annotations but does not affect rules. Pin Ruff through development dependencies or the CI installer. Upgrades may add diagnostics, correct false positives, and change formatting; make them in a dedicated, reviewable pull request.
Diagnose inconsistent results
If a rule unexpectedly appears or disappears, run Ruff against one file and identify which configuration it discovered. Nested projects may contain multiple configuration files, and command-line selections can override expectations.
When CI fails but a workstation passes, compare the Ruff version, working directory, and files analyzed before changing rules. Then inspect target version and exclusions. Running the exact CI command from the repository root usually reveals the difference.
Before merging a new policy, test a clean checkout, verify that the formatter produces no diff, and run the test suite after automated fixes. Ruff then becomes more than a fast executable: it is a small, explicit, reproducible quality contract for the repository.