Skip to content

How to migrate from Black to Ruff formatter

Ruff’s ruff format command is a drop-in replacement for Black. It follows the same formatting style, reads the same defaults (88-character lines, double quotes, space indentation), and formats 10-100x faster. Ruff also handles linting and import sorting, so the migration is often a chance to consolidate three tools (Black, flake8, isort) into one.

This guide covers translating Black’s configuration, reformatting the codebase, updating pre-commit hooks, and updating CI.

Check your starting point

  • Commit any pending changes. The migration reformats files, so starting from a clean working tree makes the diff easy to review.
  • Note your current Black version. Ruff’s formatter tracks Black’s stable style; the reformatting diff should be empty or nearly empty if your project already runs a recent Black release.

Add Ruff to the project

Add Ruff as a development dependency:

$ uv add --dev ruff

Remove Black:

$ uv remove --dev black

If the project uses requirements files instead, delete the black entry from requirements-dev.txt (or equivalent) and add ruff.

Translate your Black configuration

Ruff splits configuration across two tables in pyproject.toml: top-level [tool.ruff] for settings that apply to the whole tool (line length, target version, exclusions) and [tool.ruff.format] for formatter-specific keys (quote style, magic-trailing-comma behavior, preview). Ruff’s formatter matches Black’s default style for line length (88), indentation (four spaces), and quotes (double), so most Black projects only carry over a handful of keys.

Target-version handling is where the two tools diverge most. Black reads project.requires-python and otherwise auto-detects a version per file. Ruff also reads project.requires-python when present, but when neither that nor target-version is set it falls back to py310. Projects that still support older Python versions should set target-version explicitly.

A typical Black configuration:

pyproject.toml
[tool.black]
line-length = 100
target-version = ["py311"]
skip-string-normalization = true
extend-exclude = '''
/(
  | migrations
  | vendor
)/
'''

Becomes:

pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py311"
extend-exclude = ["migrations", "vendor"]

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

Key translations:

Black option ([tool.black]) Ruff equivalent Ruff table
line-length line-length [tool.ruff]
target-version = ["py311"] target-version = "py311" (string, not list) [tool.ruff]
extend-exclude (regex) extend-exclude (list of glob patterns) [tool.ruff]
force-exclude = true force-exclude = true [tool.ruff]
skip-string-normalization = true quote-style = "preserve" [tool.ruff.format]
skip-magic-trailing-comma = true skip-magic-trailing-comma = true [tool.ruff.format]
preview = true preview = true [tool.ruff.format]

Note

Ruff’s target-version takes a single string, not a list. If neither target-version nor project.requires-python is set, Ruff falls back to py310, which can silently enable rewrites that break older runtimes.

Reformat the codebase

Run the formatter against the whole project:

$ uv run ruff format .

If the project was already consistently Black-formatted, the diff should be empty or limited to the deliberate differences listed in Ruff’s Black-compatibility notes. Common intentional differences include how Ruff handles implicit string concatenation, f-string expressions, and parentheses around single-element tuples. Review the diff, commit it as a single “style: migrate to Ruff formatter” commit, and configure .git-blame-ignore-revs so the reformat doesn’t pollute git blame:

$ echo "$(git rev-parse HEAD)" >> .git-blame-ignore-revs
$ git config blame.ignoreRevsFile .git-blame-ignore-revs

Update pre-commit hooks

Replace the psf/black hook with astral-sh/ruff-pre-commit. Ruff’s pre-commit repo exposes separate ruff-format and ruff-check hooks so linting and formatting can run independently.

Before:

.pre-commit-config.yaml
repos:
  - repo: https://github.com/psf/black
    rev: 24.10.0
    hooks:
      - id: black

After:

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

Run pre-commit autoupdate afterwards to pin the latest Ruff release. If the project only wants formatting (not linting) for now, omit the ruff-check hook.

Update CI

Replace the Black step in GitHub Actions (or equivalent) with a Ruff step. A typical GitHub Actions step:

.github/workflows/ci.yml
- name: Check formatting
  run: uv run ruff format --check .

- name: Lint
  run: uv run ruff check .

ruff format --check exits non-zero when files need reformatting; ruff format --diff additionally prints the pending changes.

Update editor integrations

Most editors support both Black and Ruff. Switch the formatter binding:

  • VS Code: install the Ruff extension and set "editor.defaultFormatter": "charliermarsh.ruff". See How to configure VS Code for a uv project for the full setup.
  • PyCharm: 2025.3 and newer include Ruff support via LSP integration; older versions need the Ruff plugin from the marketplace. Disable the Black integration either way.
  • Neovim/Vim: point your formatter (conform.nvim, null-ls, ALE) at ruff format instead of black.

Map Black commands to Ruff

Black Ruff
black . ruff format .
black --check . ruff format --check .
black --diff . ruff format --diff .
black file.py ruff format file.py
black --line-length 100 . ruff format --line-length 100 .
black --skip-string-normalization . Configure quote-style = "preserve" in [tool.ruff.format]

Handle the known differences

Ruff’s formatter intentionally deviates from Black in a handful of cases. Most are invisible or better, but a few can cause noisy diffs on existing code:

  • f-string internals: Ruff formats expressions inside f-strings; Black leaves them alone. A line like f"{x+1}" becomes f"{x + 1}".
  • Implicit string concatenation: Ruff merges adjacent string literals onto one line if they fit. Black preserves them.
  • Single-element tuples: Ruff always parenthesizes (x,); Black sometimes removes the parentheses.
  • Trailing comments: Ruff keeps statements expanded if a trailing comment would otherwise move; Black collapses them.

If any of these produce undesirable diffs, suppress the reformat with a # fmt: off / # fmt: on block around the affected code.

Learn More

Last updated on

Please submit corrections and feedback...