Ruff replaces black, isort, pycln and flake8 (2026-03-10)¶
Until March 2026 the pre-commit configuration ran four Python tools in
sequence: black to format, isort to order imports (with the black
profile), pycln to strip unused imports, and flake8 to lint, each pinned
separately and each with its own configuration surface (.flake8,
arguments in the hook list, a pinned black in the dev dependencies).
A pyrightconfig.json also sat in the tree with nothing running it.
Decision¶
Ruff is the one Python formatter and linter. Pre-commit runs
ruff check --fix --unsafe-fixes and then ruff format; djlint handles
the templates as before. Ruff's configuration lives in pyproject.toml
under [tool.ruff]: rule sets E, W, F and I, line length 88
with E501 ignored, combine-as-imports for the import sorter, double
quotes. Everything is auto-fixable, and F401 (unused import) is an
unsafe fix so the --unsafe-fixes hook removes it, which is what pycln
used to do.
Since 2026-10-05, [tool.ruff] also excludes */migrations/*, matching
the exclude: that pre-commit already had, so running ruff over a
directory no longer rewrites generated migration files. The same commit
deleted the dead .flake8 and pyrightconfig.json, raised the
target-version to the Python the project actually requires (3.13),
and made the development installer run pre-commit install.
Alternatives¶
- Keeping the four-tool chain. It worked, but every tool had to be
pinned and updated on its own, and the import sorter and formatter
had to be kept in agreement by hand (the
--profile blackargument). The reason for the switch beyond consolidation is not recorded in the commit. - Running ruff from the virtual environment. It runs as a
systempre-commit hook, so it must be onPATH(uv tool install ruff); without it every commit fails with "ruff: command not found". The setup page and the installer both put it there. - Leaving the migrations exclusion to pre-commit alone. For seven months
a hand-run
ruff format apps/reformatted every migration that the hook would have skipped, which is why the guidance was "lint only the files you edited". The exclusion in[tool.ruff]removed that hazard.
Consequences¶
- There is no black, isort, flake8 or pyright. Do not add a configuration file for any of them; AGENTS.md says so to coding agents.
- A hook that reformats a file fails the commit and leaves the change
unstaged. Review the diff,
git add -u, commit again. - Rule changes go in
[tool.ruff]and nowhere else. Adding a rule set means the next--all-filesrun may touch many files; do that in its own commit. - Migrations are never reformatted, by either path. A migration that trips a lint rule is left as Django generated it.
- Lint the files you changed rather than a directory: the exclusion keeps ruff out of migrations, but a directory run still touches files that are not yours.
Evidence¶
- Commit "feat: integrated ruff (#393)" (2026-03-10): removes the black,
isort, pycln and flake8 hooks from
.pre-commit-config.yaml, adds theruff-pre-commithooks, replacesblackwithruffin the dev dependencies and adds[tool.ruff]topyproject.toml. - Commit "tooling: drop dead lint config, require Python 3.13, keep ruff
out of migrations, install the hook" (2026-10-05): "[tool.ruff]
excludes /migrations/, so a directory run no longer rewrites the
migrations"; deletes
.flake8andpyrightconfig.json. pyproject.toml,[tool.ruff]: "Generated code; pre-commit skips it too, soruff format apps/must not rewrite every migration."AGENTS.md: "There is no black, isort, flake8 or pyright."docs/dev/setup.md, "Pre-commit and ruff".
Related¶
- Setting up a development environment
- Branches and releases, "The pre-commit hook rewrites your commit"