# How to replace Black, isort, flake8, and pyupgrade with Ruff


[Ruff](https://pydevtools.com/handbook/reference/ruff.md) replaces [Black](https://pydevtools.com/handbook/reference/black.md), isort, [flake8](https://pydevtools.com/handbook/reference/flake8.md) (with its plugins), and pyupgrade with one binary and one configuration table. This guide walks through the consolidation, including the awkward case of keeping flake8 around to run one in-house custom plugin.

A typical 2010s Python project assembles its lint stack one tool at a time and ends up with config scattered across `pyproject.toml`, `setup.cfg`, `.flake8`, and `.pre-commit-config.yaml`. Ruff folds every rule set those tools provided into stable prefix codes inside a single binary. The formatter is Black-compatible and the import sorter matches isort; the `UP` category covers what pyupgrade did.

## Map your current tools to Ruff rule prefixes

Ruff organizes rules under short prefix codes that mirror the upstream tool a rule set originated from. Auditing the project's existing config first makes the migration mechanical: each tool maps to one or two prefixes, and the union becomes Ruff's `select` list.

| Existing tool | Ruff replacement | Notes |
|---|---|---|
| Black | `ruff format` | Drop-in formatter. See [How to migrate from Black to Ruff formatter](https://pydevtools.com/handbook/how-to/how-to-migrate-from-black-to-ruff-formatter.md) for the full formatter-knob translation. |
| isort | `I` rules + `[tool.ruff.lint.isort]` | Same import grouping. See [How to sort Python imports with Ruff](https://pydevtools.com/handbook/how-to/how-to-sort-python-imports-with-ruff.md). |
| pyupgrade | `UP` rules | The rewrites depend on `target-version`. Set it (or rely on `project.requires-python`) to match what `pyupgrade --pyXX-plus` was using, otherwise Ruff defaults to `py310` and may apply rewrites that break older runtimes. |
| pycodestyle (flake8 default) | `E`, `W` | Style errors and warnings. |
| pyflakes (flake8 default) | `F` | Logic errors: unused imports, undefined names. |
| pep8-naming | `N` | |
| flake8-bugbear | `B` | |
| flake8-comprehensions | `C4` | |
| flake8-pytest-style | `PT` | |
| flake8-quotes | `Q` | |
| flake8-implicit-str-concat | `ISC` | |
| flake8-tidy-imports | `TID` | Includes `flake8-tidy-imports.banned-api` for forbidding specific imports. |
| flake8-logging | `LOG` | |
| flake8-debugger | `T10` | |
| flake8-executable | `EXE` | |
| flake8-gettext | `INT` | |
| flake8-slots | `SLOT` | |
| flake8-raise | `RSE` | |
| flake8-logging-format | `G` | |
| flynt | `FLY` | |

For categories not in this table (Bandit security rules, docstring style, etc.), look up the prefix in the [Ruff rules index](https://docs.astral.sh/ruff/rules/).

## Add Ruff and remove the old tools

Add Ruff as a development dependency:

```console
$ uv add --dev ruff
```

Remove the tools Ruff replaces. Include every flake8 plugin the project uses; Ruff implements them natively, so the separate distributions are no longer needed:

```console
$ uv remove --dev black isort pyupgrade flake8 \
    flake8-bugbear flake8-comprehensions flake8-pytest-style \
    pep8-naming flake8-quotes flake8-tidy-imports
```

If the project uses [requirements files](https://pydevtools.com/handbook/reference/requirements.md) instead, delete the matching entries from `requirements-dev.txt` (or equivalent) and add `ruff`.

Ruff doesn't need separate isort or pyupgrade entries. The `I` and `UP` rule categories are part of the same binary; only `[tool.ruff.lint.isort]` carries forward as a config sub-table.

## Translate your configuration

Old configuration is usually scattered across two or three files. Delete it and replace it with one `[tool.ruff]` block.

Drop the following:

- `[tool.black]` and `[tool.isort]` tables in `pyproject.toml`.
- The `[flake8]` section in `setup.cfg` or the entire `.flake8` file.
- Any `pyupgrade` entries in `.pre-commit-config.yaml`.

Add a single Ruff configuration. The shape below is what most projects need:

```toml {filename="pyproject.toml"}
[tool.ruff]
target-version = "py310"
line-length = 88

[tool.ruff.lint]
select = [
  "E", "W", "F",   # pycodestyle + pyflakes (flake8's defaults)
  "I",             # isort
  "UP",            # pyupgrade
  "N",             # pep8-naming
  "B",             # flake8-bugbear
  "C4",            # flake8-comprehensions
  "PT",            # flake8-pytest-style
  "Q",             # flake8-quotes
  "ISC",           # flake8-implicit-str-concat
  "TID",           # flake8-tidy-imports
  "FLY",           # flynt
  "RUF",           # ruff-specific rules
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["TID252"]  # tests use relative imports

[tool.ruff.lint.isort]
combine-as-imports = true
known-first-party = ["myproject", "tests"]
```

Trim or extend `select` to match what the project's old configs actually enabled; there is no benefit to turning on rules nobody is going to fix today. The [Ruff rules index](https://docs.astral.sh/ruff/rules/) lists every category and rule.

> [!NOTE]
> Set `target-version` explicitly. Ruff also reads `project.requires-python`, but if neither is set it falls back to `py310`, and `UP` rules can rewrite syntax in ways that break older runtimes.

## Run the lint and format

Apply auto-fixes for everything the migration just enabled, then run the formatter:

```console
$ uv run ruff check --fix .
$ uv run ruff format .
```

The first run typically produces a large diff: import sorting, quote-style changes, and any pyupgrade-style rewrites that the project never enabled. Some fixes Ruff classifies as unsafe (for example, rewriting `"Hello, %s" % name` into an f-string), and these are skipped by default. Audit the suggestions with `--unsafe-fixes` or apply them by hand:

```console
$ uv run ruff check --diff --unsafe-fixes .
```

Commit the reformat as a single style commit and add its SHA to `.git-blame-ignore-revs` so it doesn't pollute `git blame`. The exact commands are in [How to migrate from Black to Ruff formatter](https://pydevtools.com/handbook/how-to/how-to-migrate-from-black-to-ruff-formatter.md#reformat-the-codebase).

## Update pre-commit and CI

Replace the Black, isort, flake8, and pyupgrade hooks in `.pre-commit-config.yaml` with the [astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit) repo, which exposes both `ruff-check` and `ruff-format`:

```yaml {filename=".pre-commit-config.yaml"}
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.14
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format
```

See [How to set up pre-commit hooks for a Python project](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md) for the wider hook setup.

In CI, replace the multi-tool lint step with two Ruff invocations. A typical [GitHub Actions](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) job:

```yaml {filename=".github/workflows/ci.yml"}
- name: Lint
  run: uv run ruff check --output-format=github .

- name: Format
  run: uv run ruff format --check .
```

`--output-format=github` emits GitHub Actions workflow commands so violations appear as inline annotations on the pull request Files tab instead of being buried in the run log. The same effect is available via the `RUFF_OUTPUT_FORMAT=github` environment variable, which is convenient when the same `bin/lint` script is shared between local and CI runs.

## Keep flake8 only for a custom plugin

Most projects can drop flake8 entirely. The exception is a project that ships its own flake8 plugin: a small package that publishes lint rules under a code prefix (say, `WH001`, `WH002`) and gets registered through `pyproject.toml` entry points. Ruff's plugin API is still in development under [astral-sh/ruff#283](https://github.com/astral-sh/ruff/issues/283), so for now those custom rules continue to need flake8 as their runner.

When the two tools coexist, Ruff's `RUF100` rule (unused-noqa) treats every `# noqa: WH...` comment as an unused suppression for an unknown code and strips it on `ruff check --fix`. The fix is to declare `WH` as an external code:

```toml {filename="pyproject.toml"}
[tool.ruff.lint]
external = ["WH"]
```

Per the [Ruff `lint.external` setting](https://docs.astral.sh/ruff/settings/#lint_external), this preserves `# noqa` directives whose codes Ruff doesn't recognize. A quick demonstration confirms the behavior. With the default config, Ruff strips the directive:

```console
$ cat plugin_demo.py
x = 1  # noqa: WH001
$ uv run ruff check --fix plugin_demo.py
Found 1 error (1 fixed, 0 remaining).
$ cat plugin_demo.py
x = 1
```

Add `external = ["WH"]` and the directive survives:

```console
$ uv run ruff check --fix plugin_demo.py
All checks passed!
$ cat plugin_demo.py
x = 1  # noqa: WH001
```

[PyPI's Warehouse codebase used this exact pattern](https://github.com/pypi/warehouse/pull/19943) when it migrated to Ruff in April 2026, retiring Black, isort, pyupgrade, and most flake8 plugins while keeping flake8 to run an internal `WH`-prefixed plugin. The PR description reports the four retired tools dropping from roughly 25 seconds to 6 seconds, and notes that almost all of that remaining 6 seconds is the flake8 invocation for the custom plugin. Ruff itself runs in roughly 35ms with a warm cache.

## Map old commands to Ruff

| Old command | Ruff equivalent |
|---|---|
| `black .` | `ruff format .` |
| `black --check --diff .` | `ruff format --check --diff .` |
| `isort .` | `ruff check --select I --fix .` |
| `isort --check .` | `ruff check --select I .` |
| `flake8 .` | `ruff check .` |
| `pyupgrade --py310-plus file.py` | `ruff check --select UP --fix file.py` (with `target-version = "py310"`) |
| `flake8 --select B,C4 .` | `ruff check --select B,C4 .` |

## Learn More

- [Ruff reference](https://pydevtools.com/handbook/reference/ruff.md)
- [Ruff: A Complete Guide](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md)
- [How to migrate from Black to Ruff formatter](https://pydevtools.com/handbook/how-to/how-to-migrate-from-black-to-ruff-formatter.md)
- [How to sort Python imports with Ruff](https://pydevtools.com/handbook/how-to/how-to-sort-python-imports-with-ruff.md)
- [How to configure recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md)
- [Ruff `lint.external` setting](https://docs.astral.sh/ruff/settings/#lint_external)
- [Ruff rules index](https://docs.astral.sh/ruff/rules/)
- [PyPI/Warehouse PR #19943](https://github.com/pypi/warehouse/pull/19943) (real-world migration example)
