The tool you reach for every time you save a Python file can shape how your whole team works, beyond how the code looks. Ruff, Black, and Flake8 all deal with “code quality” on paper. In practice they do very different jobs, and picking the wrong one for your workflow costs more time than most people admit.
This comparison skips the benchmark wars and looks at the part that actually matters day to day: how each tool fits into a real workflow. Where they run, what they change, how much you have to configure, and what happens when a teammate pushes code that looks nothing like yours.
Three tools, three different jobs
The single most common mistake is treating these tools as interchangeable. They aren’t.
Black is a formatter. It rewrites your code into one consistent style and stops there. It doesn’t tell you about unused imports or undefined variables. It just makes everything look the same.
Flake8 is a linter. It reads your code and reports problems: style violations, unused variables, suspicious patterns. It never rewrites anything. You fix the code yourself.
Ruff does both. It ships a linter (`ruff check`) and a formatter (`ruff format`). The formatter is designed to be compatible with Black, and the linter reimplements a large chunk of Flake8’s rules. That overlap is why the comparison exists at all.
| Tool | Formats code | Reports lint errors | Language | Config file |
|---|---|---|---|---|
| Black | Yes | No | Python | pyproject.toml |
| Flake8 | No | Yes | Python | setup.cfg, tox.ini, .flake8 |
| Ruff | Yes | Yes | Rust | pyproject.toml, ruff.toml |
That table is the whole argument in miniature. If you want one tool, Ruff is the only one that covers both columns. If you want strict separation of concerns, Black plus Flake8 still does that cleanly.
Why speed changes more than you’d think

Ruff is written in Rust, and the project reports it running orders of magnitude faster than the Python-based tools on large codebases. Treat any specific multiplier as benchmark-dependent and check the project’s own benchmark page with its date before quoting a number in a slide deck.
But speed isn’t really about the stopwatch. It’s about behavior. When a lint run takes a fraction of a second, people run it constantly: on save, on every commit, in the editor. When it takes several seconds, they run it before pushing and hope for the best. The same tool with different latency produces a different habit. (Lint (software)))
This is the quiet workflow shift. Fast tools get used more, and tools used more catch more problems earlier. If your team currently runs Flake8 only in CI because local runs feel sluggish, that’s a signal worth paying attention to.
The configuration question nobody warns you about
Here’s a practical detail that trips up almost every migration: the config files don’t match.
Flake8 traditionally reads `setup.cfg`, `tox.ini`, or `.flake8`, all in INI format. Black and Ruff read `pyproject.toml`, which is TOML. So moving from Flake8 to Ruff isn’t just swapping a command in your pre-commit file. You’re translating settings from one format to another, and some Flake8 options simply have no direct equivalent.
A few things that actually work in practice:
- Start by listing every Flake8 plugin you currently depend on, including plugins beyond the core rules.
- Check each one against Ruff’s rule index before you commit to a migration date.
- Move config into `pyproject.toml` first, keep Flake8 running, and compare output for a week.
- Only then remove the old tool from pre-commit and CI.
That last step matters. Running both tools side by side for a short window surfaces the rules Ruff doesn’t implement before your team discovers them the hard way in a pull request.
Black’s config philosophy is the opposite of Flake8’s
Black deliberately exposes very few options. That’s not laziness; it’s the design. The whole point is to end the formatting debate by removing the knobs. Flake8 sits at the other extreme, highly configurable and plugin-driven, which means two Flake8 setups in two repos can behave completely differently.
Ruff lands somewhere in the middle. It’s configurable, but it pushes you toward a single `pyproject.toml`. For teams that have suffered through “why does this file pass in repo A and fail in repo B,” that consolidation is worth more than any speed gain.
Stability, diffs, and the trust problem
Formatters touch every line of code, which makes their stability policy a workflow issue, not a style preference. Black documents a stability promise: formatting changes only land in yearly style releases, so teams can pin a version and expect stable diffs between those releases. You can read Black’s explanation of its future style policy to see exactly how it handles change.
Why does that matter? Because a formatter that shifts its output unexpectedly creates a giant diff that buries the actual logic change. Reviewers hate it. Git blame gets messy. People start dreading the upgrade.
Ruff’s formatter is newer, so the practical question for 2027 is how closely its output tracks Black and how it handles style changes over time. “Black-compatible” is a project claim that holds for most inputs but not all. Known divergences exist in edge cases. If you’re switching, run the formatter on a branch, diff the result, and look at what actually changed before merging anything.
A sensible workflow habit: never combine a formatter upgrade with a feature branch. Format in its own commit, with nothing else in it. That one rule saves hours of review pain regardless of which tool you choose.
Where each tool fits in a real pipeline
The glue for all three is pre-commit, the framework that runs checks before a commit lands. Ruff publishes its own pre-commit configuration, and Black and Flake8 have official hooks in the shared hooks repository. That means the integration story is roughly equal, and the decision comes down to what you want running at each stage.
A workflow that holds up well:
- On save, in the editor: formatter runs automatically. Nobody thinks about it.
- On commit: linter runs on changed files only, fast enough that nobody is tempted to skip it with `–no-verify`.
- In CI: full lint plus a format check that fails if anything is unformatted.
The format check in CI is the piece people forget. Without it, someone with a different editor setup sneaks unformatted code in, and the next person’s format-everything commit becomes a mess. If you’re already comfortable tuning local tooling, the same “small friction, big payoff” logic shows up in other setups, like the tricks behind a keyboard foam mod and the sound differences it actually changes or how Obsidian vs Logseq local-first notes compare by workflow. The pattern is the same: pick the setup that fits how you already work, not the one with the longest feature list.
Migration traps worth knowing
Three mistakes show up again and again.
First, assuming rule parity. Ruff implements a large subset of Flake8’s pycodestyle and pyflakes rules plus many plugin sets, but not every third-party Flake8 plugin has a Ruff equivalent. Audit before you cut over. (Ruff documentation)
Second, ignoring rule code differences. Your `# noqa` comments and per-file ignores reference specific codes. Some map cleanly, some don’t. A silent mismatch means a rule you thought was suppressed is now firing, or worse, the reverse.
Third, forgetting the editor. Your team’s IDE integrations, save actions, and format-on-type settings all need updating. A migration that only touches CI leaves half the team formatting differently on save.
So which one should you pick in 2027?
There’s no universal winner, but there are clear decision paths.
Pick Ruff alone if you want one tool, one config file, and the fastest feedback loop. It’s the natural choice for new projects and for teams tired of maintaining parallel formatter and linter setups.
Pick Black plus Flake8 if you value strict separation of concerns, you depend on a Flake8 plugin with no Ruff equivalent, or your organization has years of Flake8 config baked into CI that nobody wants to touch.
Pick Ruff for linting and Black for formatting if you want the speed where it hurts most (lint runs) but prefer Black’s longer track record on formatting stability. This hybrid is more common than people admit, and it’s a perfectly reasonable middle ground.
One caution for 2027 specifically: don’t assert that one tool has fully replaced another or that a project is deprecated unless you’ve checked current release notes. Feature parity and deprecation status change, and a confident claim that’s six months stale undermines the whole comparison.
If your team is also weighing hardware and software upgrade decisions this year, the same “does the difference actually matter daily” test applies to things like the iPhone 17 vs iPhone 16 upgrade differences that matter daily. Tools earn their place by changing daily behavior, not by winning spec sheets.
Conclusion
The Ruff vs Black vs Flake8 question isn’t really about which tool is best. It’s about which division of labor fits your team. Black formats and stays out of the way. Flake8 lints and reports. Ruff does both, faster, in one config file, with a few compatibility caveats worth checking before you bet the pipeline on it.
The practical move for most teams in 2027 is to run both setups side by side for a week, compare the output, and let the diffs decide. That’s a far better use of an afternoon than another round of benchmark arguments.
FAQ
Is Ruff a drop-in replacement for Black and Flake8?
Mostly, but not perfectly. Ruff’s formatter is designed to be Black-compatible and its linter covers a large share of Flake8 rules, yet both are project claims that hold for most inputs rather than all. Known divergences exist in formatting edge cases, and some Flake8 plugins have no Ruff equivalent. Test on a branch before switching.
Does Black report lint errors?
No. Black is a formatter only. It rewrites code into a consistent style and says nothing about unused imports, undefined names, or other lint issues. You need a separate linter like Flake8 or `ruff check` for that.
Can I use Ruff just for linting and keep Black for formatting?
Yes, and plenty of teams do. It gives you fast lint feedback while keeping Black’s longer track record on formatting stability. The main cost is maintaining two tools and two configs instead of one.
Why is Ruff so much faster than Flake8?
Ruff is written in Rust, while Flake8 and Black are written in Python. The project reports large speedups on big codebases, but treat specific multipliers as benchmark-dependent and check the current benchmark page with its date before quoting a number.
Do I have to move my config to pyproject.toml?
If you adopt Ruff or Black, practically yes. Flake8 traditionally reads INI-format files like `setup.cfg` or `.flake8`, while Black and Ruff read TOML. Migration usually means translating settings, and some Flake8 options have no direct equivalent.
How do I avoid a giant formatting diff when switching tools?
Format in a dedicated commit with no logic changes, and keep formatter upgrades separate from feature branches. Run the new formatter on a branch first, review the diff, and only then merge. Many teams also add a format check in CI so unformatted code can’t slip back in.
That side-by-side week is the cheapest insurance you can buy. Keep the old tool’s output as a baseline, diff it against the new tool’s output file by file, and log every discrepancy with its rule code. Most will be trivial whitespace or import-order differences; the handful that aren’t are exactly the ones you need to resolve before cutover. Once the diff is clean, delete the baseline, drop the old hook, and update the team’s editor configs in the same pull request so nobody is left formatting on save with a tool the pipeline no longer runs.


