TechQuant Blog

Ruffを使い倒すための実践Tips — black/flake8/isortから乗り換えて1年経ったメモ

7分で読める

auto_daily_trader のリポジトリで blackisortflake8 の順に走らせる pre-commit が、コミットごとに 6 秒くらい待たされていた。ファイル数 200 程度の、別に大きくもないプロジェクトでこれだ。Raspberry Pi 5 上だと体感はさらに鈍い。ある日 ruff に置き換えてみたら、3 つまとめて 0.2 秒で終わった。「まあ速くはなるだろう」程度に思っていたので、桁違いの数字を見て普通に笑ってしまった。

それから 1 年。新規プロジェクトはほぼ全部 ruff に統一して、既存も少しずつ移してきた。この記事はその過程で「最初から知っていたら良かった」と思った設定とルール選びの話だ。

対象は black/flake8/isort あたりを使ってきて、ruff にまだ移っていない人。すでに ruff を入れているけど、ルールの選び方で迷っている人にも刺さるはず。

なぜ ruff に乗り換えるのか

速さは確かにインパクトがある。けれど 1 年使って一番効いたのは、別の点だった。

  • ツールが 1 つになるpyproject.toml の設定セクションが 3 つから 1 つに減る。これがじわじわ効く
  • pre-commit の体感が変わる。0.2 秒なら「忘れてた」が起きない
  • flake8 系プラグインの大半が内蔵flake8-bugbearpep8-namingpyupgrade 相当が標準で入っている
  • autofix が強いruff check --fix でかなりの違反を自動修正してくれる

逆に最初の頃ハマったのは、ルールの粒度が細かすぎる点だ。何も考えずに select = ["ALL"] を入れると、自分のコードの全行が真っ赤になる。これについては後述する。

インストールと最低限の設定

導入は pip install ruff でも入るが、自分は uv 経由でプロジェクトに突っ込んでいる。uv add --dev ruffpyproject.toml に dev 依存として残るので扱いやすい。

uv add --dev ruff
uv run ruff check .
uv run ruff format .

pyproject.toml の設定は最初こうしている。

[tool.ruff]
line-length = 100
target-version = "py312"
extend-exclude = ["migrations", ".venv", "build"]

[tool.ruff.lint]
select = [
  "E",   # pycodestyle errors
  "W",   # pycodestyle warnings
  "F",   # pyflakes
  "I",   # isort
  "B",   # flake8-bugbear
  "UP",  # pyupgrade
  "SIM", # flake8-simplify
  "C4",  # flake8-comprehensions
]
ignore = [
  "E501",  # line too long (formatterに任せる)
  "B008",  # FastAPI の Depends でぶつかる
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "ANN"]  # assert と型注釈は緩める

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

line-length は 88(black デフォルト)よりちょい広い 100 にしている。Pi 5 の小さい画面で書く時にも収まりやすいのと、SQL の埋め込みで折り返し過多になるのを避けたい、という個人的事情だ。ここは好みでいい。

ルール選定の考え方

ALL は避ける、必要なものを足す

ruff のルールは 800 を超える。select = ["ALL"] にすると、docstring が無いとか、関数の引数の数が多いとか、果ては「copyright が無い」みたいなものまで全部弾かれる。新規プロジェクトでも辛い。既存に当てたら詰む。

自分は最初に上の 8 カテゴリだけ入れて、運用しながら気になったものを足す方式に落ち着いた。1 ヶ月くらい回して「これが入っていれば前のあのバグは出なかったな」と思ったルールを select に追加していく感じ。

追加で刺さったルール

  • S(flake8-bandit): セキュリティ系。subprocessshell=True や弱い乱数生成を拾ってくれる
  • RUF(ruff 独自): asyncio.create_task の戻り値を捨てる、みたいな async まわりの罠を見つける
  • PT(flake8-pytest-style): pytest.fixture() の括弧揺れみたいな細かいやつを統一
  • TID(flake8-tidy-imports): 相対インポートの深さを制限する。チームで方針を揃えやすい
  • PL(pylint 抜粋): 一部のロジック系チェック。ただし全部入れると煩いので個別 select

per-file-ignores は素直に使う

テストコードに S101(assert 禁止) を効かせると意味が無い。CLI スクリプトに T201(print 禁止) を効かせるのも違う。per-file-ignores で素直に黙らせる方が、生産性も読み手の理解も早い。

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101", "ANN", "PLR2004"]
"scripts/**/*.py" = ["T201"]
"__init__.py" = ["F401"]  # re-export

ruff format の話

ruff には formatter が同梱されていて、これが black の挙動とほぼ互換だ。ほぼと書いたのは、いくつか細部で差があるから。実害が出たケースは自分の経験では一度も無いが、知らないと「あれ、行の入り方が変わった」と一瞬戸惑うことはある。

ruff format .             # 全部整形
ruff format --check .     # チェックのみ(CIで使う)
ruff format --diff .      # 差分表示

black 2024 系から ruff format に乗り換える時、git のコミット履歴を汚さないために、移行コミットを 1 本だけ作って .git-blame-ignore-revs に入れておくのが無難。git blame がそのコミットを飛ばしてくれるので、責任の所在が見やすくなる。

# .git-blame-ignore-revs
abc1234567...  # ruff format への一括移行

pre-commit と VS Code 連携

pre-commit

.pre-commit-config.yaml はこれだけでいい。

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

--fix を付けると、コミットしようとした瞬間に直せる違反は勝手に直してから止めてくれる。停止理由が明確で、次の git add && git commit で通るようになる。直前まで flake8 が「直してね」と言うだけで止めていたのを思い出すと、地味な進化だが効く。

VS Code

公式拡張 charliermarsh.ruff を入れて、settings.json に保存時整形を仕込む。

{
  "[python]": {
    "editor.defaultFormatter": "charliermarsh.ruff",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.fixAll.ruff": "explicit",
      "source.organizeImports.ruff": "explicit"
    }
  }
}

organizeImports を effectful に入れていないと、import の並び替えが反映されない。ここは最初ハマった。「保存しても整列しないんだけど?」と 30 分くらい設定をいじり倒した記憶がある。

CI への組み込み

GitHub Actions では CI 設定の記事でも触れた構成にもう一段ステップを足すだけ。

- name: Lint with ruff
  run: |
    uv run ruff check --output-format=github .
    uv run ruff format --check .

--output-format=github を付けると、PR 上の該当行にアノテーションが出る。これが地味に便利で、「どの行で何が引っかかったか」がレビュー画面で直接見える。仕組みとしては大したことが無いのに、レビュー往復回数がはっきり減った。

ローカルで make lint 一発で済ませたい派なので、Makefile 側にもターゲットを置いている。

.PHONY: lint format
lint:
	uv run ruff check .
	uv run ruff format --check .

format:
	uv run ruff check --fix .
	uv run ruff format .

既存プロジェクトに導入する順序

これは経験則だが、一気にやると後悔する。自分は 3 段階に分けてやっている。

  1. Step 1: ruff format を black 互換設定で当てて、整形だけ済ませた巨大コミットを 1 本作る。.git-blame-ignore-revs に登録
  2. Step 2: ruff check --select F,E,W,I --fix で機械的に直せるものを片付ける。残った警告を見て頭を整理する
  3. Step 3: B, UP, SIM あたりを少しずつ足す。CI で blocker にするのはここまで通った後

Step 2 で大量の警告が残る場合、ruff check --statistics でルール別の件数を出し、件数が多いものから片付ける順序を決めると効率がいい。

ruff check --statistics .

ハマりどころ

noqa の書式が変わる

flake8 の # noqa: E501 はそのまま動くが、ruff は内部のコード体系を持っているので、ルール名と noqa コードがズレるケースがたまにある。違反コードを正確に書くには ruff check --add-noqa でファイルに自動付与してしまうのが楽。

ruff check --add-noqa .

autofix が壊すパターンがゼロではない

正直に書くと、SIM 系の autofix がたまに「意味的にちょっと違う」変換を入れることがある。例えば、副作用のあるオブジェクトに対して if x in (a, b) 形式に書き換えて挙動が変わる、みたいなやつ。回数はかなり少ないが、autofix を当てた後の差分を git diff で必ず一度は通すクセを付けたほうがいい。盲信しないこと。

ルールがバージョンで増える

ruff はリリース頻度が高い。バージョンを上げたらいきなり CI が赤くなる、ということが起きる。pre-commit autoupdate を Renovate や Dependabot 経由で PR にしておけば、差分が見える形で取り込めるので楽。

1 年使ってみての主観

ここは正直に言うと、まだ移行しきれていない。古い社内ツールはまだ black + flake8 のまま。理由はシンプルで、誰も困っていないからだ。動いているものを移すコストとリターンが見合うかは別問題で、ruff が速いからといって全部今すぐ移すべき、とまでは思っていない。

ただ新規プロジェクトに関しては、もう ruff 以外を選ぶ理由が思いつかない。uv + ruff の組み合わせは Pi 5 みたいな非力な環境でも体感が軽くて、コミット直前のあの「待ち」がほぼ消える。これは実際に手を動かす時間に効いてくる。

まだ ruff を試していないなら、新規ブランチで uv add --dev ruff && uv run ruff check . だけ叩いてみてほしい。最初の出力を眺めるだけで、ある程度感触はつかめるはず。そこから select を絞り込んで、自分のコードベースの「クセ」と相談しながらルールを育てていくのが、結局一番早く馴染む道筋だった。