Four tools, four config files, four version pins, and a startup cost you pay on every commit. Ruff replaces black, flake8, isort and most of their plugins with one binary — and the migration is a couple of hours, not a project.
The stack most Python teams inherited is black for formatting, flake8 for linting, isort for import ordering, and a handful of flake8 plugins bolted on for good measure. Each is a separate dependency, each has its own configuration surface, and each spins up its own Python process when it runs. Ruff — written in Rust by the Astral team — does the work of all four, reads one config, and runs fast enough that the linter stops being the slow step in your pre-commit hook. This is a straight consolidation exercise: you are not changing what your team agreed the code should look like, you are changing how many tools enforce it. Done carefully, the diff at the end is small.
What actually maps to what
Before touching anything, understand the mapping, because it is what makes the migration safe rather than a guess. Ruff groups its rules into families identified by a letter prefix, and each family corresponds to a tool or a flake8 plugin you are already running:
Free · 4 minutes
Do you actually know what you are running — and what it is about to cost you?
Fourteen questions on the systems you depend on, the ones nobody owns, and the support dates that turn a routine upgrade into a forced re-platform. Banded finding on screen, full sheet by email.
- F is Pyflakes — the undefined-name and unused-import checks that are the core of flake8.
- E and W are pycodestyle — the style and whitespace warnings.
- I is isort — import sorting and grouping, no separate tool required.
- B is flake8-bugbear, C4 is flake8-comprehensions, UP is pyupgrade, SIM is flake8-simplify — the common plugins, folded in.
The formatter is separate from the linter and is what replaces black. Ruff’s formatter is deliberately black-compatible: same 88-character default line length, same double-quote preference, same magic trailing comma behaviour. In practice reformatting an already-black codebase with Ruff produces a near-empty diff. That compatibility is the whole reason this is low-risk.
The ruff.toml that does all four jobs
Create a ruff.toml at the project root (or a [tool.ruff] block in pyproject.toml if you prefer one file). This is the config that replaces your .flake8, your isort settings and your black config in one place:
# ruff.toml — replaces black, flake8, isort and their plugins
target-version = "py311"
line-length = 88 # black's default; keep it to avoid a reformat storm
[lint]
# Port your old flake8 selection. Each family maps to a tool or plugin:
# E, W -> pycodestyle F -> pyflakes I -> isort
# B -> bugbear C4 -> comprehensions UP -> pyupgrade
# SIM -> flake8-simplify
select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM"]
ignore = [
"E501", # line length — the formatter owns wrapping now
]
[lint.per-file-ignores]
"__init__.py" = ["F401"] # allow re-exports without a noqa on each line
"tests/*" = ["B011"]
[lint.isort]
known-first-party = ["myapp"]
combine-as-imports = true
[format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
A few deliberate choices in there. E501 is ignored because the formatter handles line wrapping; leaving it enabled means the linter nags about long lines the formatter has chosen not to break — long string literals and URLs, mostly — which is noise. The [lint.isort] section is where your old isort config goes: set known-first-party to your own package so imports group correctly. And note that Ruff does not enable pycodestyle warnings (W) or complexity checks by default, so if you relied on those under flake8 you have to select them explicitly, as above.
Running the migration
Install Ruff, pin it, and drop the four old dependencies from your dev requirements in the same commit. Then run the two commands that do the work:
# pin it — Ruff moves fast and stabilises formatting per release
uv pip install "ruff==0.8.6"
ruff format . # replaces: black .
ruff check --fix . # replaces: flake8 + isort, and auto-fixes what it can
Pinning the exact version matters more than it does for flake8. Ruff’s formatter can adjust its output between minor releases, so an unpinned Ruff in CI can produce a formatting diff nobody made locally. Commit a lockfile or an exact pin and bump it deliberately. Review the first --fix diff by hand — most of it will be import reordering and a scatter of pyupgrade and bugbear fixes — then commit the tool swap and the reformatted code separately so the history stays readable.
Wiring it into pre-commit and CI
The point of the exercise is enforcement, not a one-off cleanup. Replace the black, flake8 and isort hooks in .pre-commit-config.yaml with a single Ruff repo. Order matters: the linter runs first with --fix, then the formatter, so formatting is applied to already-fixed code.
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6 # keep in lockstep with your pinned ruff
hooks:
- id: ruff # lint; runs before the formatter
args: [--fix]
- id: ruff-format # replaces the black hook
One naming wrinkle worth knowing: recent releases of ruff-pre-commit renamed the lint hook id from ruff to ruff-check, with the old name kept as an alias. Match the id to the rev you pin. In CI, enforce rather than fix — ruff check . and ruff format --check . both exit non-zero on a violation without modifying files, which is exactly what you want a pull-request gate to do. If you already run a pre-commit baseline that actually runs in CI, this is a two-line change to that config.
The payoff is not just a shorter dependency list, though four fewer packages and their transitive plugins is a real reduction in the surface you have to keep patched. It is that the linting step effectively disappears from your feedback loop — the process startup cost of running four separate Python tools is gone, and on a large repository that is the difference between a pre-commit hook people wait for and one they skip. Faster checks are one of the quieter inputs to lead time: the pipeline stage nobody notices is the one that was slowing everyone down.
Consolidating onto Ruff is one of the cheapest technical-debt wins available to a Python team — a bounded, reversible change that removes three tools and leaves the code looking identical. The only way to get it wrong is to leave it unpinned.
Build and rescue work
Hands-on delivery of this kind is handled by Sixteen Pillars Studio.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Most technology problems are not technology problems. They are control problems.
The systems exist. The investment has been made. The question is whether leadership can understand, direct, evidence, and sustain what those systems produce. Find out where control exists — and where it only appears to.