TL;DR

Ruff is one Rust binary that does the job of Flake8, Black, isort, and pyupgrade at once, and it runs 10–100x faster than any of them (Astral’s reported figure). For 90% of Python projects it’s the correct default in 2026: replace the whole lint-and-format stack with ruff check and ruff format, delete four dev dependencies, and move on. It does not replace Pylint’s deep static analysis, and it does no type checking at all, so heavy codebases still pair it with mypy or pyright. If you’re starting a new project today, reach for Ruff first and add the others only when you hit a wall.

FactValueSourceVerified
Latest Ruff version0.16.6github.com/astral-sh/ruffSep 7, 2026
Built-in lint rules900+docs.astral.sh/ruffSep 7, 2026
Speed vs Flake8/Black10–100x faster (reported)astral.sh/ruffSep 7, 2026
Black formatting parity>99.9% lines identical (reported)docs.astral.sh/ruffSep 7, 2026
Type checkingNone (use mypy/pyright/ty)docs.astral.sh/ruffSep 7, 2026

I deleted four tools from one repo and nothing broke

A few weeks ago I ripped Flake8, Black, isort, and pyupgrade out of a ~35k-line internal service and replaced all four with Ruff. The .pre-commit-config.yaml went from six hooks to one. What I didn’t expect was how quiet the change was: the diff Ruff’s formatter produced on our already-Black-formatted code touched 11 lines across the whole repo, all of them end-of-line comment spacing. Everything else was byte-identical.

The part that sold me came later. Our pre-commit run used to take around 14 seconds on a full-repo pass, long enough that people skipped it with --no-verify when they were in a hurry. With Ruff the same pass finished in under half a second in my runs. Nobody skips a hook that costs nothing, and the benchmark charts undersell that part: a linter fast enough that people stop reaching for --no-verify catches more real bugs than one extra rule ever will.

The milliseconds are the least of it. The real question is whether the four-tool Python linting stack everyone copied from everyone else since 2019 still earns its place, and for most repos it no longer does.

What each tool was actually for

Before Ruff, a well-run Python repo usually stacked four or five separate tools, each solving one slice of the problem:

  • Flake8 — the linter. Catches unused imports, undefined names, style violations (it wraps pycodestyle and pyflakes). Extensible through a large plugin library (flake8-bugbear, flake8-comprehensions, and dozens more).
  • Black — the formatter. Opinionated, near-zero config, reformats your code to one canonical style so nobody argues about it in review.
  • isort — sorts and groups imports. Everyone forgets to do this by hand.
  • pyupgrade — rewrites old syntax to modern equivalents ('{}'.format(x) to f-strings, typing.List to list on modern Python).
  • Pylint — the deep analyzer. Complexity scoring, dead-code detection, refactoring suggestions, the works. Slower and noisier than Flake8, but it sees things Flake8 never will.

Each tool has its own config file, its own CLI, its own cache, and its own place in your pre-commit chain. That’s four subprocess spawns and four passes over your source tree every time you save. Ruff’s whole pitch is that there’s no reason for that in 2026.

How big the speed gap actually is

Ruff is written in Rust and lints in a single pass. Astral, the company behind it (and behind uv, the package manager I compared against pip and Poetry), reports 10–100x speedups over Flake8 and Black. Their own testimonials go further: one cites a 250k-line module going from 2.5 minutes with Pylint to 0.4 seconds with Ruff, and another reports flake8’s 20-second scan dropping to 0.2 seconds. Treat the extreme numbers as vendor-reported, because they depend heavily on codebase shape and which rules you enable.

~14s → 0.4s
My repo's pre-commit pass
10–100x
Faster than Flake8/Black (reported)
900+
Built-in lint rules

Where that speed actually shows up: on a small file you’ll never notice. On a Python monorepo pre-commit hook, or in CI across thousands of files, or when your editor lints on every keystroke, 20 seconds is long enough that people start avoiding the linter, and a fifth of a second is short enough that they forget it runs at all. I’ve watched teams disable editor linting entirely because Flake8 lagged behind their typing. Ruff’s LSP keeps up.

What Ruff replaces, and how completely

“One tool to rule them all” is marketing. The table below sorts the clean replacements from the partial ones:

CapabilityOld toolRuff replaces it?Notes
Linting (pyflakes/pycodestyle)Flake8YesAll core rules reimplemented natively
Flake8 pluginsflake8-bugbear, -comprehensions, etc.Mostly40+ popular plugins reimplemented; a few niche ones aren’t
FormattingBlackYes>99.9% identical output on Black-formatted code (reported)
Import sortingisortYesNear-equivalent to isort’s profile = "black"
Syntax upgradespyupgradeYesThe UP rule family
Dead-code removalautoflakeYesUnused imports/variables via --fix
Deep static analysisPylintNoComplexity, duplicate-code, and refactor suggestions still need Pylint
Type checkingmypy / pyrightNoRuff is explicit: it is not a type checker

The Flake8 and Black replacements are the ones that hold up cleanly. Per Astral’s own FAQ, the formatter is a drop-in for Black with over 99.9% of lines identical on projects like Django and Zulip, and Ruff works as a drop-in for Flake8 when you’re not leaning on exotic plugins. Import sorting matches isort’s Black profile with a couple of known differences around aliased imports and inline comments.

Pylint is the honest exception. It does whole-program analysis Ruff deliberately doesn’t attempt: cyclomatic complexity thresholds, detecting that two functions are 90% duplicated, catching that you’re calling a method that doesn’t exist on a class. If you rely on those, keep Pylint, and just run it less often, because it’s the slow one.

Where Ruff still loses

I like Ruff. The gaps are worth naming anyway, because a comparison that only lists the wins isn’t worth reading.

The big one is type checking, or rather the complete absence of it. Ruff will catch an undefined name, but it’ll happily let you pass a str where an int is expected. Astral’s guidance is to run Ruff alongside a type checker, never in place of one. If you want to see how those stack up, I went deep on the type-checker side in ty vs mypy vs pyright and the follow-up on Pyrefly vs mypy vs ty; the short version is that Ruff and a type checker do different jobs and a serious project runs both.

Pylint also still sees more. On a large legacy codebase where you actually want the complexity report and the “this looks like a bug” heuristics, it stays sharper than Ruff’s PL rule family, which ports a chunk of Pylint’s checks but skips the whole-program analysis that needs a full call graph. There’s a plugin wrinkle too: if your CI leans on a niche Flake8 plugin Ruff hasn’t reimplemented, you’ll either keep Flake8 for that one check or drop it. Most teams find they don’t miss it, and the ones that do keep the single plugin around.

Then there’s churn. Ruff ships often (0.16.6 as of this writing, with releases landing every few weeks), so rules get promoted from preview, defaults shift, and once in a while a rule you relied on moves. Pin the version in CI and read the release notes when you bump it. That’s the tax for a tool still moving this fast.

Migrating in an afternoon

This is the part that convinced my team it was low-risk. Here’s the actual sequence.

First, install Ruff and drop a config into pyproject.toml:

[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
# Start conservative: pyflakes (F), pycodestyle (E/W),
# bugbear (B), import sorting (I), pyupgrade (UP).
select = ["E", "W", "F", "B", "I", "UP"]
ignore = ["E501"]  # let the formatter own line length

[tool.ruff.format]
quote-style = "double"

Then run the two commands once and commit the result:

# auto-fix everything safe, then format
ruff check --fix .
ruff format .

A ruff check run on a repo with issues looks like this:

$ ruff check .
app/services/billing.py:12:1: F401 [*] `datetime.timezone` imported but unused
app/services/billing.py:88:5: B008 Do not perform function call `Depends` in argument defaults
app/models/user.py:3:1: I001 [*] Import block is un-sorted or un-formatted
app/utils/dates.py:41:9: UP032 [*] Use f-string instead of `format` call
Found 4 errors.
[*] 3 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).

The [*] marks the ones Ruff can fix for you. Notice B008 isn’t auto-fixable: it’s a genuine bug-class warning (a mutable default evaluated once at import time), and Ruff correctly refuses to guess your intent there.

Finally, collapse the pre-commit config. Before:

repos:
  - repo: https://github.com/psf/black
    rev: 24.10.0
    hooks: [{id: black}]
  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks: [{id: isort}]
  - repo: https://github.com/pycqa/flake8
    rev: 7.1.1
    hooks: [{id: flake8}]
  - repo: https://github.com/asottile/pyupgrade
    rev: v3.19.0
    hooks: [{id: pyupgrade}]

After:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.16.6
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

Four repos become one. The first full run reformats anything the old tools missed; after that, the hook is effectively free. The one gotcha I hit: if your team had a heavily customized setup.cfg for Flake8, port those select/ignore choices deliberatly rather than trusting the defaults, because Ruff’s default rule set is broader than stock Flake8 and will surface warnings you’d silenced years ago.

Which one should you actually run?

Your situationRun this
New project, 2026Ruff (check + format) as the default, nothing else
Existing Flake8 + Black + isort stackMigrate to Ruff; keep a type checker
Large legacy codebase needing complexity/dead-code analysisRuff for fast feedback + Pylint on a slower cadence
You need type safetyRuff + mypy or pyright (Ruff does not type check)
Niche Flake8 plugin with no Ruff equivalentRuff + that one Flake8 plugin

For most people reading this, row one is the answer. Ruff replaced the stack on the repos I maintain, and I haven’t wanted the old tools back.

FAQ

Is Ruff faster than Flake8?

Yes, dramatically. Astral reports 10–100x speedups, and real-world testimonials cite scans dropping from ~20 seconds to ~0.2 seconds on large codebases. In my own repo, a full pre-commit pass went from around 14 seconds to under half a second. The gap comes from Ruff being a single Rust binary that lints in one pass instead of spawning several Python tools.

Does Ruff replace Black?

Yes. Ruff’s formatter is designed as a drop-in for Black, and Astral reports over 99.9% of lines format identically on Black-formatted projects. You’ll see minor differences on non-Black code, mostly around end-of-line comments, but for practical purposes ruff format and black produce the same output.

Does Ruff replace Pylint?

No, not fully. Ruff ports some Pylint checks in its PL rule family, but it doesn’t do the deep whole-program analysis Pylint is known for: complexity scoring, duplicate-code detection, and heuristics that need a full call graph. Use Ruff for fast everyday linting and keep Pylint if you depend on that analysis.

Does Ruff replace isort?

Yes. Ruff’s import sorting (the I rules) is near-equivalent to isort’s profile = "black", with a few known differences in how aliased imports and inline comments are handled. Enable it in select and drop isort from your config.

Does Ruff do type checking?

No. Ruff is explicit that it’s a linter and formatter, not a type checker. It will catch undefined names but not type mismatches. Pair it with mypy, pyright, or Astral’s own ty for type safety.

How do I configure Ruff?

Put everything in pyproject.toml under [tool.ruff]. Set line-length and target-version, choose your rule families in [tool.ruff.lint] via select, and set formatter options under [tool.ruff.format]. Ruff reads a single config file, so you can delete the separate setup.cfg, .flake8, .isort.cfg, and Black sections you used to maintain.

Sources

Bottom line

The four-tool Python linting stack was a workaround for a language that didn’t have one good tool. Now it does. Ruff won’t type-check for you and won’t out-analyze Pylint on a gnarly legacy codebase, and you should keep those tools where they earn their keep. Everywhere else, the recommendation is simple: install Ruff, run the two commands, delete the rest, and enjoy a pre-commit hook nobody wants to skip.