# How to migrate from Black to Ruff formatter


[Ruff](https://pydevtools.com/handbook/reference/ruff.md)'s `ruff format` command is a drop-in replacement for [Black](https://pydevtools.com/handbook/reference/black.md). 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](https://pydevtools.com/handbook/reference/flake8.md), isort) into one.

This guide covers translating Black's configuration, reformatting the codebase, updating [pre-commit hooks](https://pydevtools.com/handbook/how-to/how-to-set-up-pre-commit-hooks-for-a-python-project.md), 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:

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

Remove Black:

```console
$ uv remove --dev black
```

If the project uses [requirements files](https://pydevtools.com/handbook/reference/requirements.md) 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](https://pydevtools.com/handbook/reference/pyproject.toml.md): 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:

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

Becomes:

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

```console
$ 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](https://docs.astral.sh/ruff/formatter/black/). 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`:

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

```yaml {filename=".pre-commit-config.yaml"}
repos:
  - repo: https://github.com/psf/black
    rev: 24.10.0
    hooks:
      - id: black
```

After:

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

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](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) step:

```yaml {filename=".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](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff) and set `"editor.defaultFormatter": "charliermarsh.ruff"`. See [How to configure VS Code for a uv project](https://pydevtools.com/handbook/how-to/how-to-configure-vs-code-for-a-uv-project.md) 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

- [Ruff reference](https://pydevtools.com/handbook/reference/ruff.md)
- [Black reference](https://pydevtools.com/handbook/reference/black.md)
- [Ruff: A Complete Guide](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md)
- [How to configure recommended Ruff defaults](https://pydevtools.com/handbook/how-to/how-to-configure-recommended-ruff-defaults.md)
- [How to sort Python imports with Ruff](https://pydevtools.com/handbook/how-to/how-to-sort-python-imports-with-ruff.md)
- [Ruff formatter Black-compatibility notes](https://docs.astral.sh/ruff/formatter/black/)
- [Ruff formatter configuration](https://docs.astral.sh/ruff/formatter/#configuration)
