Skip to content

Ruff vs flake8: Which Python Linter Should You Use?

Ruff and flake8 check Python code for the same class of problems: unused imports, undefined names, and PEP 8 style violations. Flake8 was the default answer for over a decade. Ruff reimplements flake8’s rule set (and most of its plugin ecosystem) in a single binary, and for new projects it is the better choice. The decision worth thinking about is whether an existing flake8 project should switch.

How do the rule sets compare?

Flake8 is an aggregator: it bundles pyflakes (F rules, logical errors), pycodestyle (E and W rules, PEP 8 style), and mccabe (complexity), and everything else comes from third-party plugins such as flake8-bugbear or flake8-comprehensions.

Ruff implements all of the F rules and the commonly used E and W rules natively, under the same codes, so # noqa: F401 comments (the inline directives that silence a rule on one line) keep working. The E and W rules it leaves out are mostly whitespace-layout checks that a formatter enforces anyway; some are available behind Ruff’s preview flag. Ruff also reimplements the popular plugins under short prefixes (B for bugbear, C4 for comprehensions, PT for pytest-style), which is how it reaches 968 rules as of version 0.15.22 (818 stable, 150 in preview).

Two default-behavior differences surprise people coming from flake8:

  • Ruff drops the rules that fight formatters. Flake8 flags E203 (whitespace before :) by default, which conflicts with Black-style formatting and lands in most projects’ extend-ignore; flake8 itself has shipped W503 (line break before binary operator) disabled by default since 2016 for the same reason. Ruff goes further: E203 is off unless preview mode is enabled, and W503 doesn’t exist at all.
  • The line-length defaults differ. Flake8 defaults to 79 characters; Ruff defaults to 88, matching Black. On pip’s codebase, that one difference accounted for 5,037 E501 violations under flake8 versus 1,821 under Ruff. (Ruff only checks line length when E501 is selected; its out-of-the-box selection doesn’t include it.)

The gap runs the other way for custom rules. Flake8 has a plugin API, so a team can ship an in-house checker; Ruff does not support custom lint rules. If your project depends on a plugin with no Ruff equivalent, that plugin is the reason to keep flake8 around.

How does configuration differ?

Flake8 reads configuration from setup.cfg, tox.ini, or a dedicated .flake8 file. It does not read pyproject.toml natively; the third-party flake8-pyproject plugin adds that.

.flake8
[flake8]
max-line-length = 88
extend-ignore = E203

Ruff configures everything in pyproject.toml (or a standalone ruff.toml), next to the rest of the project’s tool settings:

pyproject.toml
[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "W", "F", "B"]

The difference compounds with plugins. A flake8 setup grows by installing packages: each plugin is a separate dependency to pin and upgrade. In Ruff, enabling the equivalent rules is one more prefix in the select list.

How much faster is Ruff?

On pip’s codebase (about 134,000 lines of Python), with both tools set to the full E, W, F rule families:

Tool Fresh Debian container (4 vCPUs) M-series Mac
flake8 7.3.0 1.15s 1.0s
Ruff 0.15.22 (cache disabled) 0.11s 0.02-0.04s

That is a 10x gap on identical hardware, wider on fast single-core machines, and with flake8 already parallelizing across cores. Ruff also caches results, so warm re-runs on an unchanged codebase return in milliseconds. A one-second flake8 run is tolerable in continuous integration (CI); a near-zero one is what makes linting affordable on every save in the editor and in pre-commit hooks.

What does Ruff add beyond linting?

Flake8 reports problems; fixing them is manual. Ruff fixes many of its violations automatically with ruff check --fix: unused imports disappear out of the box, and with the I and UP rule sets enabled it also sorts imports and rewrites deprecated syntax.

Ruff also ships ruff format, a Black-compatible formatter, in the same binary. A project that runs flake8 alongside Black and isort can replace all three tools with one dependency. How to replace Black, isort, flake8, and pyupgrade with Ruff walks through that consolidation.

When should you keep flake8?

Three situations favor staying put:

  • Custom in-house plugins. Ruff cannot run them. Some teams keep a minimal flake8 alongside Ruff just for the custom checker, with external = ["XX"] in Ruff’s config so Ruff’s noqa cleanup rule (RUF100) doesn’t strip the plugin’s codes.
  • Plugins without Ruff equivalents. Most popular plugins are reimplemented, but check the Ruff rules index against your plugin list before assuming coverage.
  • A stable config you don’t touch. If flake8 runs in seconds in CI and nobody edits the lint config, migrating buys speed you may not need.

None of these apply to a new project. Start with Ruff; the tutorial Set up Ruff for formatting and checking your code covers the first install.

How do you migrate?

The mechanics are short: add Ruff, translate the [flake8] section into [tool.ruff.lint] (per-file-ignores has a direct equivalent there), map each plugin to its rule prefix, and remove the old dependencies. Because Ruff keeps flake8’s rule codes, existing # noqa comments carry over unchanged. Before deleting flake8, run both tools once on the same tree and diff the findings; anything only flake8 reports is a rule to select explicitly in Ruff, or a gap to accept knowingly. The step-by-step migration guide includes the full plugin-to-prefix mapping table.

Learn More

Last updated on