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:
[tool.black]
line-length = 100
target-version = ["py311"]
skip-string-normalization = true
extend-exclude = '''
/(
| migrations
| vendor
)/
'''Becomes:
[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:
repos:
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: blackAfter:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.11
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-formatRun 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:
- 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 formatinstead ofblack.
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}"becomesf"{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.