Pythonのruff入門 – pyproject.toml設定とCI組み込みの書き方

Pythonのruff入門 - pyproject.toml設定とCI組み込みの書き方 | mohablog

ruffはRust製のPython用linter/formatterで、flake8・black・isort・pyupgradeなどを1コマンドに統合できます。現行は0.15.13(2026年5月14日リリース)。Astral社の公式サイトはpandasコードベースを0.5秒でlintできると示しており、flake8の30秒と比べて60倍の差。導入からCI組み込みまでをpyproject.tomlの実例で整理します。

目次

ruffが置き換える4つのツールと速度差

ruffは複数のPython向け静的解析ツールをRustで再実装しています。役割と公式が示す速度差は次の表のとおり。

従来ツール役割ruffでの吸収速度差(公式値)
flake8lintruff checkpandasで約60倍
blackformatruff format10〜100倍
isortimport整理lint側のIルール同梱
pyupgrade構文モダン化UPルール群同梱

flake8のプラグイン群(flake8-bugbear、flake8-comprehensions、flake8-simplifyなど)も内部に取り込み済み。1つのRust製バイナリで完結するため、requirements-dev.txtからこれらPython製ツールの依存をまるごと外せます。

インストールから初回実行までの手順

pipとuvどちらでも入る

ruffはバイナリ配布。Pythonへの依存追加は実質ゼロです。

# uv 派 (推奨)
uv tool install ruff

# pip 派
pip install ruff

ruff --version

実行結果:

ruff 0.15.13

ruff check と ruff format の最小コマンド

ruff checkがlinter、ruff formatがformatter。引数なしで現在ディレクトリ以下を再帰的に走査します。

$ ruff check .
src/utils.py:3:1: F401 [*] `json` imported but unused
src/handlers.py:42:5: E731 Do not assign a `lambda` expression
Found 2 errors.
[*] 1 fixable with the `--fix` option.

初回はデフォルトルールだけが有効になります。公式の“Inferring the Python version”節に記載のとおり、Pyflakes(F)とpycodestyleのE4/E7/E9のみ。900以上ある全ルールのうちごく一部だけが有効な状態で始まります。

pyproject.tomlでルール選択とignoreを書く

selectとignoreの使い分け

有効化したいルール群は[tool.ruff.lint]select、特定の違反だけ抑止したいときはignore。両者は同じlintセクションに並べて書きます。

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = [
  "E",   # pycodestyle errors
  "F",   # Pyflakes
  "I",   # isort
  "B",   # flake8-bugbear
  "UP",  # pyupgrade
  "SIM", # flake8-simplify
]
ignore = ["E501"]  # 行長はformatterに任せる

実行サマリで何件・どのルールが引っかかったかを確認:

$ ruff check --statistics src/
   3	F401	[*] unused-import
   1	UP015	[*] redundant-open-modes
   2	SIM108	[*] if-else-block-instead-of-if-exp
Found 6 errors.
[*] 6 fixable with the `--fix` option.

per-file-ignoresでテストだけ緩める

本体には厳しく、テストやマイグレーションには緩く、というファイル別の緩和はper-file-ignoresで書きます。テストのassert禁止(S101)や、自動生成コードに対するルール除外はここに集約。

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "D"]   # assert許可、docstringチェック停止
"__init__.py" = ["F401"]    # 再エクスポートを許可
"migrations/*" = ["ALL"]    # 自動生成コードは対象外

extend-selectで段階的に厳しくする

既存プロジェクトへ途中導入する場合、最小ルールから始めてextend-selectで順次広げます。一度に全ルールを有効化すると修正PRが肥大化。レビュー粒度が崩れる原因になります。実際に約12万行のPythonリポジトリでEFから入れた後、月次でIUPSIMと段階的に追加した経験では、各段階のPRが200〜400ファイル程度で収まりました。

[tool.ruff.lint]
select = ["E", "F"]            # 最初はここだけ
extend-select = ["I", "UP"]    # 次の段階で足す

Fix safetyを理解する – safeとunsafeの違い

ruffは公式ドキュメントの“Fix safety”章で自動修正をsafeunsafeに分類しています。

The meaning and intent of your code will be retained when applying safe fixes.

safe fixはコードの振る舞いを変えない範囲の修正。デフォルトの--fixはsafe fixだけを適用します。unsafe fixは挙動が変わる可能性があるため、--unsafe-fixesを明示しない限り走りません。

unsafe fixの具体例

公式が示す例: list(...)[0]next(iter(...))に書き換えるとき、空コレクションだとIndexErrorではなくStopIterationが飛びます。例外型が変わるためcatch側が破損し得る。これがunsafe扱いの理由です。

ruff check --fix .                # safeのみ適用
ruff check --fix --unsafe-fixes . # unsafeも含めて適用

noqaで個別に抑制する

行単位の抑制はnoqaコメント。ファイル単位で全部止めるなら冒頭にruff: noqa、特定ルールだけ無効化するならruff: noqa: F841を1行入れます。

import json  # noqa: F401  再エクスポート目的で残す
x = eval(s)  # noqa  どのルールにも引っかけない

ブロック単位の抑制(0.6系以降)は# ruff: disable[E501]# ruff: enable[E501]のペアで挟みます。生成コードや長い文字列リテラルだけ局所的に外したいときに便利。

ruff formatでblackから移行する差分

ruff formatは公式ドキュメント“Philosophy”章でBlack互換を掲げています。ほぼ同じ出力を出しつつ、公式が“Intentional deviations”として残している意図的な違いも存在。代表的な3点が次のh3。

f-string内部の式まで整形する

blackはf-stringの式部分に介入しません。ruffはクオート種別や折り返しをf-string内まで揃えます。

# before (blackでは残るパターン)
msg = f"{user[ 'name' ]}: {len( items )}件"

# ruff format 後
msg = f"{user['name']}: {len(items)}件"

docstring内のコードブロックを整形する

docstring-code-format = trueを有効にすると、docstring中のフェンス付きコードブロックまで整形対象。公開ライブラリのAPIリファレンスでよくある>>>サンプルやコード例も、本体コードと同じ書式に揃います。

[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = 80
quote-style = "double"

magic trailing commaの扱い

末尾カンマで強制改行する仕様(magic trailing comma)はblackと同じ動作。無効化したい場合はskip-magic-trailing-comma = trueを入れます。

CIで失敗させる – –check と –exit-non-zero-on-fix

CIでフォーマット崩れを検知したいときはruff format --check、lintで自動修正可能な違反があるだけで失敗させたいならruff check --exit-non-zero-on-fixを使います。

GitHub Actionsの最小例

- name: ruff lint
  run: uv tool run ruff check .

- name: ruff format check
  run: uv tool run ruff format --check .

差分が見つかった場合の出力:

Would reformat: src/handlers.py
Would reformat: tests/test_utils.py
2 files would be reformatted, 18 files already formatted
Error: Process completed with exit code 1.

pre-commitに繋ぐ

公式リポジトリruff-pre-commitを利用。commit直前にruff check --fixruff formatを実行し、自動修正があればそのままcommitに含めます。revのバージョン固定が必須のため、updateはdependabotかrenovateに任せる運用が無理なく回ります。

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.13
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

VSCodeで保存時に走らせる設定

VSCode公式拡張Ruff(Astral社製)を入れ、ワークスペース設定で保存時のformatとfixを有効化。設定の本体はcodeActionsOnSaveにあります。

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

保存のたびにimport並び替えとsafe fixが走る挙動。チームで揃える場合は.vscode/settings.jsonをリポジトリにcommitすれば全員に同じ挙動が配布されます。explicitを指定する理由は、保存ショートカットに対してだけ反応させ、外部ツールからのファイル書き換えで誤発火させないため。

まとめ

  • ruff 0.15.13はruff checkruff formatでlint・format・import整理・構文モダン化を一括化
  • pyproject.tomlの[tool.ruff.lint]selectignoreを書き、per-file-ignoresでテストだけ緩める構成が扱いやすい
  • 自動修正はsafeunsafeに分かれ、unsafeは--unsafe-fixesを明示するまで走らない
  • blackからの移行はf-string内整形・docstring内コード整形・magic trailing commaの3点を押さえれば差分は局所化
  • CIはruff format --checkruff check --exit-non-zero-on-fixで失敗させ、ローカルはpre-commitとVSCodeのcodeActionsOnSaveで揃える
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次