# Ruff vs flake8: Which Python Linter Should You Use?


[Ruff](https://pydevtools.com/handbook/reference/ruff.md) and [flake8](https://pydevtools.com/handbook/reference/flake8.md) check Python code for the same class of problems: unused imports, undefined names, and [PEP 8](https://pydevtools.com/handbook/explanation/what-is-pep-8.md) 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](https://pydevtools.com/handbook/reference/black.md)-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](https://pydevtools.com/handbook/reference/pyproject.toml.md) natively; the third-party flake8-pyproject plugin adds that.

```ini {filename=".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:

```toml {filename="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](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md).

## 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](https://pydevtools.com/handbook/how-to/how-to-replace-black-isort-flake8-pyupgrade-with-ruff.md) 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](https://docs.astral.sh/ruff/rules/) 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](https://pydevtools.com/handbook/tutorial/set-up-ruff-for-formatting-and-checking-your-code.md) 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](https://pydevtools.com/handbook/how-to/how-to-replace-black-isort-flake8-pyupgrade-with-ruff.md) includes the full plugin-to-prefix mapping table.

## Learn More

* [Ruff](https://pydevtools.com/handbook/reference/ruff.md) and [flake8](https://pydevtools.com/handbook/reference/flake8.md) reference pages
* [Ruff: a complete guide](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) covers rule categories and configuration in depth
* [How do Ruff and Pylint compare?](https://pydevtools.com/handbook/explanation/how-do-ruff-and-pylint-compare.md) for the other legacy-linter comparison
* [Ruff FAQ](https://docs.astral.sh/ruff/faq/) states the drop-in-replacement conditions and native plugin list
