Skip to content

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

Ruff replaces Black, isort, flake8 (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 for the full formatter-knob translation.
isort I rules + [tool.ruff.lint.isort] Same import grouping. See How to sort Python imports with Ruff.
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.

Add Ruff and remove the old tools

Add Ruff as a development dependency:

$ 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:

$ 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 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:

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 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:

$ 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:

$ 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.

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 repo, which exposes both ruff-check and ruff-format:

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

See How to set up pre-commit hooks for a Python project for the wider hook setup.

In CI, replace the multi-tool lint step with two Ruff invocations. A typical GitHub Actions job:

.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, 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:

pyproject.toml
[tool.ruff.lint]
external = ["WH"]

Per the Ruff lint.external setting, this preserves # noqa directives whose codes Ruff doesn’t recognize. A quick demonstration confirms the behavior. With the default config, Ruff strips the directive:

$ 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:

$ 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 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

Last updated on

Please submit corrections and feedback...