【Python】flake8 + black + isort から Ruff 0.17 へ移行したログ — 実際に壊れた4箇所

3つのツールが1つのノードに統合される様子を抽象化したアイキャッチ画像
目次

flake8 + black + isort を Ruff に寄せたい

  • flake8・black・isort の3ツールを Ruff に寄せたいが、設定のどこをどう写すのか分からない
  • Ruff を上げた瞬間に既定ルールが変わって、CI が身に覚えのない警告で落ちた
  • 「black 互換」と聞くけれど、自分のコードで本当に同じ出力になるのか確かめたい

対象読者: flake8 / black / isort で lint と整形を回している Python プロジェクトの担当者。

前提知識: pyproject.toml の読み方と、# noqa が何を抑止するコメントなのかが分かっていれば読めます。

結論

移行は「pyproject.toml に [tool.ruff] を1つ書く → ruff check --fix → ruff format」の3ステップです。

ただし、いきなりバージョンを上げるのは危険です。Ruff 0.17 で既定の target-version と既定ルールセットが変わったので、この2つを設定で固定してから上げると差分が荒れません。

# 1. 3ツールを1つに畳む
uv add --dev ruff

# 2. まず現状把握(既定ルールで何が出るか見る)
uv run ruff check --statistics .
# 実行結果
# 4	UP006 	[*] non-pep585-annotation
# 2	UP035 	[ ] deprecated-import
# 1	UP045 	[*] non-pep604-annotation-optional
# 1	UP007 	[*] non-pep604-annotation-union
# 1	F405 	[ ] undefined-local-with-import-star-usage
# Found 9 errors.
# [*] 6 fixable with `--fix`.

# 3. 安全な修正だけ当てて、整形する
uv run ruff check --fix .
# 実行結果
# Found 9 errors (6 fixed, 3 remaining).
uv run ruff format .
# 実行結果
# 1 file reformatted

これで CI に置くコマンドは ruff check . と ruff format --check . の2本だけになります。

順番が大事です。先に設定を固定してからバージョンを上げると、壊れた箇所が「設定の不足」なのか「バージョンの変化」なのか切り分けられます。

移行前の構成

検証用に、flake8 + black + isort で運用してきた小さなプロジェクトを用意しました。設定は3枚に分かれています。

[flake8]
max-line-length = 100
extend-ignore = E203,W503
exclude = .git,__pycache__,build,dist
per-file-ignores =
    __init__.py:F401
[tool.black]
line-length = 100

[tool.isort]
profile = "black"
line_length = 100
flake8==7.4.1
black==26.10.1
isort==9.0.2
flake8-bugbear
flake8-comprehensions

依存は5つ、設定は3枚、CI のジョブは3本という状態です。これを実際に回した結果がこちら。

flake8 src
# 実行結果
# src\legacyapp\app.py:22:16: F405 'join' may be undefined, or defined from star imports: os.path

black --check -q src
# 実行結果(出力なし・終了コード 1 = 整形が必要)

isort --check-only src
# 実行結果(出力なし・終了コード 0 = 並び順は OK)

flake8 の指摘は1件、black は整形が必要、isort は通っています。この3つを1本にまとめるのがゴールです。

CI で直列に回すと、失敗した時点で後続が止まります。flake8 が落ちれば black の結果は見えません。

Ruff が置き換えるもの・置き換えないもの

移行を決める前に、守備範囲を確認しておきます。Ruff は flake8 本体と主要プラグイン、black、isort、pyupgrade を1つのバイナリに畳みます。

  • 置き換わる: flake8(pyflakes / pycodestyle)・flake8-bugbear・flake8-comprehensions・pep8-naming・pydocstyle・pyupgrade・isort・flake8-bandit 相当(S ルール)
  • 置き換わらない: 型チェック(mypy / pyright / ty は別途必要)
  • 置き換わらない: 自作の flake8 プラグイン(Ruff はサードパーティのルールを読み込めません)

自作プラグインを1つだけ使っている場合は、そこだけ flake8 を残す構成もあります。公式 FAQ にも「カスタム lint ルールには対応しない」と明記されています。

移行手順

3枚の設定は pyproject.toml の1ブロックに畳めます。

[tool.ruff]
line-length = 100
src = ["src"]

[tool.ruff.lint]
select = ["E", "W", "F", "I", "UP", "B", "C4"]

[tool.ruff.lint.isort]
known-first-party = ["legacyapp"]

旧設定との対応は次のとおりです。pre-commit を使っている場合は、フックも3本から ruff と ruff-format の2本に減ります。

移行前移行後
.flake8 の max-line-length[tool.ruff] の line-length
.flake8 の extend-ignore[tool.ruff.lint] の ignore
.flake8 の per-file-ignores[tool.ruff.lint.per-file-ignores](書式は同じ)
[tool.black] の line-length[tool.ruff] の line-length
[tool.isort] の profile = “black”不要(I ルールが isort 相当)
[tool.isort] の line_length[tool.ruff.lint.isort] の line-length
flake8 .ruff check .
black .ruff format .
isort .ruff check –select I –fix .

設定を書いた直後に ruff check を回すと、いきなり9件の警告が出ます。

ruff check --statistics .
# 実行結果
# 4	UP006 	[*] non-pep585-annotation
# 2	UP035 	[ ] deprecated-import
# 1	UP045 	[*] non-pep604-annotation-optional
# 1	UP007 	[*] non-pep604-annotation-union
# 1	F405 	[ ] undefined-local-with-import-star-usage
# Found 9 errors.
# [*] 6 fixable with `--fix`.

内訳を見ると、flake8 が見つけていた F405 のほかに UP 系が8件あります。UP 系は pyupgrade 相当で、flake8 には無かった観点です。

--fix を付けると安全な修正だけが当たり、3件が残ります。

ruff check --fix .
# 実行結果
# Found 9 errors (6 fixed, 3 remaining).

ruff check .
# 実行結果(抜粋)
# UP035 `typing.Dict` is deprecated, use `dict` instead
# UP035 `typing.List` is deprecated, use `list` instead
# F405 `join` may be undefined, or defined from star imports

残った3件は自動修正の対象外です。UP035 は「typing の古い別名を消す」修正なので、その名前がまだ使われているかを Ruff 側で判断できません。F405 も同様に、人間が import を書き換える必要があります。

実際に壊れた4箇所

3ツールを畳む作業そのものは淡々と進みました。引っかかったのは、Ruff 0.16 から 0.17 に上げたときの4箇所です。

1. 既定の target-version が 3.10 から 3.11 に変わった

Ruff は requires-python も target-version も見つけられないとき、既定値を使います。0.17 でその既定が上がりました。

ruff 0.16.10 check --isolated --show-settings src/legacyapp/app.py
# 実行結果(抜粋)
# formatter.unresolved_target_version = 3.10
# analyze.target_version = 3.10

ruff 0.17.0 check --isolated --show-settings src/legacyapp/app.py
# 実行結果(抜粋)
# formatter.unresolved_target_version = 3.11
# analyze.target_version = 3.11

3.10 と 3.11 では UP 系の提案内容が変わります。指定漏れのまま上げると、差分の理由が読めません。

2. 既定ルールセットから DTZ が外れ、F406 が入った

0.17 のリリースノートには「既定のルールセットを更新」と明記されています。設定を読ませずに --isolated で回すと、差がそのまま出ます。

ruff 0.16.10 check --isolated --statistics src/legacyapp/app.py
# 実行結果(抜粋)
# 4	UP006 	[*] non-pep585-annotation
# 2	UP035 	[ ] deprecated-import
# 2	RUF100	[*] unused-noqa
# 1	DTZ011	[ ] call-date-today
# 1	DTZ005	[ ] call-datetime-now-without-tzinfo
# Found 12 errors.

ruff 0.17.0 check --isolated --statistics src/legacyapp/app.py
# 実行結果(抜粋)
# 4	UP006 	[*] non-pep585-annotation
# 2	UP035 	[ ] deprecated-import
# 2	RUF100	[*] unused-noqa
# Found 10 errors.

flake8-datetimez の7ルールが既定から外れ、F406(関数の中の import *)が既定に入りました。前者は警告が急に消える方向、後者は急に増える方向の変化です。

DTZ を意図的に残したい場合は select に明示します。ルール自体は消えていません。

ruff 0.17.0 check --isolated --select DTZ src/legacyapp/app.py
# 実行結果
# DTZ005 `datetime.datetime.now()` called without a `tz` argument
# DTZ011 `datetime.date.today()` used
# Found 2 errors.

3. I001 の行長計算が変わって import が組み替えられる

ここがいちばん厄介でした。0.17 では行の長さを数えるときに末尾の pragma コメント(# noqa や # type: ignore)を無視するようになりました。

結果として、0.16 では「長すぎる」と判断されて分割されていた import が、0.17 では1行に戻ります。

ruff 0.16.10 check --isolated --select I001 --line-length 60 --diff src/pragma.py
# 実行結果
# +from collections import (  # noqa: F401
# +    OrderedDict,
# +    defaultdict,
# +)
#  from datetime import date, datetime, timezone  # noqa: F401
# -from collections import OrderedDict, defaultdict  # noqa: F401
# +from typing import (  # noqa: F401
# +    Any,
# +    Dict,
# +    List,
# +    Optional,
# +    Union,
# +)
# Would fix 1 error.

同じファイルを 0.17 に通すと、分割が解けて1行に戻ります。

ruff 0.17.0 check --isolated --select I001 --line-length 60 --diff src/pragma.py
# 実行結果
# +from collections import OrderedDict, defaultdict  # noqa: F401
#  from datetime import date, datetime, timezone  # noqa: F401
# -from collections import OrderedDict, defaultdict  # noqa: F401
#  from typing import Any, Dict, List, Optional, Union  # noqa: F401
# Would fix 1 error.

0.16 で整形済みのリポジトリを 0.17 に上げると、ruff check --select I --fix で import が組み替わって CI が落ちます。リリースノートでも「既存の import が再整形される可能性があるため破壊的変更に分類した」と書かれています。

移行するときは、この整形差分を独立したコミットにしておくと後から追いやすくなります。コメント付きの import を多用しているプロジェクトほど差分は大きくなります。

4. unsafe fix の中身が既定出力に出るようになった

既定の出力形式も変わりました。安全でない修正の diff まで表示されるようになっています。

ruff 0.16.10 check --isolated src/unsafe.py
# 実行結果(抜粋)
# UP031 Use format specifiers instead of percent format
# help: Replace with format specifiers
# Found 1 error.
# No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).

ruff 0.17.0 check --isolated src/unsafe.py
# 実行結果(抜粋)
# UP031 Use format specifiers instead of percent format
#   -     return "hello %s" % name
#   +     return "hello {}".format(name)
# note: This is an unsafe fix and may change runtime behavior
# Found 1 error.
# 1 unsafe fix available with `--fix --unsafe-fixes`.

CI のログに出るテキストが変わるので、ログを正規表現で拾っている仕組みがあると、その取り込みが失敗します。0.17 では --fix を付けていなくても diff が出る点に注意してください。

つまずきポイント

isort のキー名はアンダースコア

移行前の設定を写すとき、isort 側のキーは line_length です。ハイフンで書くと isort 本体が起動時に落ちます。

isort --check-only src
# 実行結果(抜粋)
# isort.exceptions.UnsupportedSettings: isort was provided settings that it doesn't support:
# 	- line-length = 100  (source: pyproject.toml)

Ruff 側は line-length とハイフンなので、両方を行き来すると間違えやすいところです。

RUF100 を単体で select すると noqa を消しすぎる

RUF100(使われていない noqa)は、そのとき有効なルールを基準に判定します。

設定の select に足して回すと1件、--select RUF100 だけで回すと2件になります。

ruff check --extend-select RUF100 --statistics .
# 実行結果(抜粋)
# 1	RUF100	[*] unused-noqa

ruff check --select RUF100 --statistics .
# 実行結果(抜粋)
# 2	RUF100	[*] unused-noqa

後者の実行で消える noqa は実際にはまだ必要です。--select は設定の select を置き換えるので、RUF100 の判定材料も一緒に消えてしまうためです。

–isolated は設定を読まない

この記事の比較では --isolated を付けています。設定ファイルを一切読まずに既定値だけで動かすフラグで、バージョン間の既定値の差を見るのに向いています。

手元で試すときは外して、自分の [tool.ruff] を読ませてください。

UP035 は自動修正されない

統計の [ ] は「修正が用意されていない」印です。--fix を何回回しても残ります。

手で import を書き換えてください。今回のプロジェクトでは from typing import Dict, List, Optional, Union の1行を消すだけで解消しました。

移行後の結果

手で2箇所直したあと、最終的にこうなりました。

ruff check .
# 実行結果
# All checks passed!

ruff format --check .
# 実行結果
# 1 file already formatted

整形結果が black と一致するかも確かめました。同じファイルを両方に通して差分を取ると、byte 単位で一致します。

black -q black_out.py
ruff format -q ruff_out.py
diff black_out.py ruff_out.py
# 実行結果(出力なし = 差分なし)

速度も測っておきます。300ファイルのコーパスを同じマシンで回した結果です。

python bench.py
# 実行結果(抜粋)
# === 移行前: 3ツールを個別に回す ===
# flake8 corpus/                     中央値  0.805s
# black --check corpus/              中央値  1.199s
# isort --check-only corpus/         中央値  0.315s
# 3ツール合計                              2.320s
# === 移行後: ruff 1本 ===
# ruff check corpus/                 中央値  0.031s
# ruff format --check corpus/        中央値  0.029s
# ruff 合計                             0.060s
# --- 倍率: 38.5x(3ツール合計 / ruff 合計)

3ツールの合計が 2.320 秒、Ruff 1本が 0.060 秒で、38.5 倍でした。手元の実測値なので、ファイル構成や有効にするプラグインの数で変わります。

最後に .flake8 と [tool.black] / [tool.isort] のセクションを消します。設定が二重に残ると、次に触る人がどちらが正なのか分からなくなります。

まとめ

項目移行前移行後
設定ファイル.flake8 と pyproject.toml の2セクションpyproject.toml の [tool.ruff] 1ブロック
dev 依存5つ(flake8 / black / isort とプラグイン2つ)1つ(ruff)
CI のコマンド3本2本(check と format –check)
300ファイルの実行時間2.320 秒0.060 秒
black との整形差—今回のサンプルでは byte 一致

作業は「設定を1枚にする」「自動修正を当てる」「手で残りを直す」の3段階に分けてコミットしておくと、レビューが楽になります。

移行そのものは1時間で終わる規模です。危ないのは設定の書き換えではなく 0.17 の既定値の変化なので、そこだけ先に固定してから上げるのが安全ですよ。

検証環境

OSWindows 11 (build 10.0.26200)
CPUAMD Ryzen 9 PRO 8945HS w/ Radeon 780M Graphics
メモリ28GB
言語/ツールRuff 0.17.0(公式の Windows x86_64 バイナリ ruff-x86_64-pc-windows-msvc.zip を一時ディレクトリに展開) / Ruff 0.16.10(同上・比較用) / flake8 7.4.1 / black 26.10.1 / isort 9.0.2(uv venv の一時環境に導入) / Python 3.14.7(uv venv の作成元)
使用コマンドruff check –isolated –statistics(0.17.0 と 0.16.10 の比較) / ruff check –select I001 –line-length 60 –diff / flake8 src / black –check src / isort –check-only src / python bench.py(300ファイル)
測定日2026-10-13

上記の環境で実際に実行して確認した結果です。環境が異なる場合は挙動が変わることがあります。

flake8 + black + isort のサンプルプロジェクトを Ruff 0.17.0 と 0.16.10 で処理した実測ログ。既定の target-version が 3.10 から 3.11 に変わったこと、既定ルールから DTZ が外れて F406 が入ったこと、pragma コメントの行長計算が変わって import が組み替えられること、unsafe fix の中身が既定出力に出ること、isort のキー名の誤り、RUF100 を単体で select したときの誤検出、300ファイルの速度比較までを1枚にまとめたもの
flake8 + black + isort のサンプルプロジェクトを Ruff 0.17.0 と 0.16.10 で処理した実測ログ。既定の target-version が 3.10 から 3.11 に変わったこと、既定ルールから DTZ が外れて F406 が入ったこと、pragma コメントの行長計算が変わって import が組み替えられること、unsafe fix の中身が既定出力に出ること、isort のキー名の誤り、RUF100 を単体で select したときの誤検出、300ファイルの速度比較までを1枚にまとめたもの

参考

【広告】お名前.comならドメイン取得が格安。

あわせて読みたい

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次