KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

PythonのLinter & Formatter(Flake8 + isort + Black)をRuffに置き換えたら爆速でした

こんにちは、カケハシで Musubi 開発チームのバックエンドエンジニアをしている関です。

Musubi 開発では、 Python の Linter と Formatter に Flake8、isort、Black を使用しておりました。しかし Rust で書かれた Ruff という高性能なツールが出たということで、置き換えてみたら爆速になった(15倍以上速くなった)ので、Ruff について記事を書かせていただきます。

今回は Ruff を導入した経緯や実運用に至るまでの工程を紹介したいと思いますので、最後まで読んでいただけると嬉しいです。

Ruffとは

Ruff は、2022年8月にリリースされた Rust 言語で書かれた Python の Linter 兼 Formatter です。数多くのフレームワークやライブラリで採用1されています。

Python での開発には複数のツールチェーンを役割ごとに組み合わせて利用する機会が多いかと思います。 代表的なものだと、Linter には、Flake8 や Pylint、Prospector があり、Formatter には autopep8 や Black、isort などがありますが、選択肢の多さやその組み合わせの複雑さに頭を悩ませることも多いかと思います。 また、それぞれツールチェーンごとに Python ファイルの構文解析の処理が走るので、同じような処理が何度も実行されてしまい、ビルド時間が長くなってしまうというデメリットがあります。 Ruff は Linter と Formatter の機能を兼ねており、1つのツールに役割を集約できるため、構文解析も1回で済み、効率化を図ることができます。

Ruff は以下の特徴があります。(公式サイトから引用して翻訳してます) - ⚡️ 既存のリンター (Flake8 など) やフォーマッタ (Black など) よりも 10 ~ 100 倍高速 - 🐍pip経由でインストール可能 - 🛠️pyproject.tomlサポート - 🤝 Python 3.12 との互換性 - ⚖️ Flake8、 isort 、 Blackとのドロップイン同等性 - 📦 組み込みのキャッシュにより、変更されていないファイルの再分析を回避 - 🔧 自動エラー修正のサポートを修正 (例: 未使用のインポートを自動的に削除) - 📏 flake8-bugbear などの人気のある Flake8 プラグインのネイティブ再実装を備えた700以上の組み込みルール - ⌨️ VS Code、Vim、Emacs、PyCharmなどの 統合エディターとの連携 - 🌎 モノリポに適しており、階層的およびカスケード構成を備えている

こんなにいいこと尽くしなので即行で導入しました。

Ruff の導入

Musubi の開発では、 pre-commit で Linter や Formatter のチェックを行なっております。 そのため今回は pre-commit に Ruff を導入したので、その際に対応した手順を紹介します。

インストール

pre-commitで動作させるので、Ruff のインストールは必要なかったのですが、直接 Ruff を動かす際にはこちらのコマンドでインストールします。

pip install ruff

pre-commitの設定ファイル に Ruff の設定を追加

pre-commit の設定ファイルに Ruff の設定を追加しました。v0.1.0が執筆時点の最新版です。

# .pre-commit-config.yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
  rev: v0.1.0
  hooks:
    - id: ruff
      args: [--fix]
    - id: ruff-format
      types_or: [python, pyi]

Ruff でチェックするルールを設定ファイルに追加

Flake8、isort、Blackの設定を引き継いで Ruff に設定します。 具体的には、pyproject.yml に 設定を追加していきます。

selectでチェックするルールを指定していて、"I"は isort を指しています。

# pyproject.yml
[tool.ruff]
select = [
  "C9",
  "E",
  "F",
  "W",
  "I",
]

次は isort と Black の設定をしていきます。 元々の設定はこちらです。

# pyproject.yml
# Ruff 移行前
[tool.isort]
combine_as_imports = true
default_section = "THIRDPARTY"
include_trailing_comma = true
known_first_party = "musubi_restapi"
sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
line_length = 119
multi_line_output = 3
skip_glob=".venv,musubi_restapi/__init__.py"

[tool.black]
line-length = 119
target-version = ['py38']
include = '\.pyi?$'

これを Ruff の設定に置き換えたのが以下になります。

[tool.ruff.isort]の部分が、isort用の設定です。 Black は [tool.ruff][tool.ruff.format]に設定項目を追加していきます。

# pyproject.yml
# Ruff 移行後

# black用の設定
[tool.ruff]
target-version = "py38"
include = ["*.py"]
line-length = 119

# isort用の設定
[tool.ruff.isort]
combine-as-imports = true
known-first-party = ["musubi_restapi"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
split-on-trailing-comma = true

# black用の設定
[tool.ruff.format]
quote-style = "double"

最終的に、pyproject.ymlはこのようになりました。 ignoremccabeは元々の設定を引き継いでいます。

# pyproject.yml
[tool.ruff]
exclude = [
  "xxx/*.py",
  ".venv",
]
ignore = [
  "E203", # Whitespace before ':' (E203)
  "E501", # Line too long (82 > 79 characters) (E501)
  "F811", # Redefinition of unused name from line n (F811)
  "E741", # Do not define classes named 'I', 'O', or 'l' (E742)
  "E266", # too many leading '#' for block comment (E266)
  "PIE",  # flake8-pie (PIE)
  "F601", # multi-value-repeated-key-literal (F601)
  "E721", # type-comparison (E721)
]
select = [
  "C9",
  "E",
  "F",
  "W",
  "I",
]

# black用のチェックルール
target-version = "py38"
include = ["*.py"]
line-length = 119

# isort用のチェックルール
[tool.ruff.isort]
combine-as-imports = true
known-first-party = ["xxxx"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
split-on-trailing-comma = true

# black用のチェックルール
[tool.ruff.format]
quote-style = "double"

[tool.ruff.mccabe]
max-complexity = 20

参考

ちなみに、Flake8 から Ruff への移行には、flake8-to-ruff というツールが利用できます。Ruffの作者が公開しているツールです。 Flake8の設定を読み込み、Ruff用の設定ファイルを出力することができます。

flake8-to-ruff

pip install flake8-to-ruff
flake8-to-ruff path/to/.flake8

Ruff と Flake8 の混在をやむなく許容

Ruff(v0.1.0) では Flake8 のルールをすべて搭載しているわけではなかったので、ローカル環境においては、一部のルールを Flake8 でチェックをすることにしました。

CI環境(Github Actionsの Workflow)では、 実行時間削減のため Flake8 のチェックをスキップすることにしています。 SKIP という環境変数に pre-commit の hook id を設定してあげれば、その hook をスキップすることができます。

export SKIP="flake8"

Github Actionsのpre-commit用のWorkflowにこちらを設定。

- name: Set SKIP environment variable
  run: echo "SKIP=flake8" >> $GITHUB_ENV

Flake8 でやむなくチェックすることにした項目

Ruff(v0.1.0) では対応できない Flake8 のルールが以下になります。 こちらはローカル環境でのみチェックするようにしました。

[tool.flake8]
select = [
  "PIE787", # no-len-condition (PIE787)
  "E121",   # Continuation line under-indented for hanging indent (E121)
  "E122",   # Continuation line missing indentation or outdented (E122)
  "E123",   # Closing bracket does not match indentation of opening bracket's line(E123)
  "E124",   # Closing bracket does not match visual indentation(E124)
  "E125",   # Continuation line with same indent as next logical line(E125)
  "E126",   # Continuation line over-indented for hanging indent(E126)
  "E127",   # Continuation line over-indented for visual indent(E127)
  "E128",   # Continuation line under-indented for visual indent(E128)
  "E129",   # Visually indented line with same indent as next logical line(E129)
  "E131",   # Continuation line unaligned for hanging indent(E131)
  "E133",   # Closing bracket is missing indentation(E133)
  "E301",   # Expected 1 blank line, found 0(E301)
  "E303",   # Too many blank lines (3)(E303)
  "E304",   # Blank lines found after function decorator(E304)
  "E305",   # Expected 2 blank lines after end of function or class(E305)
  "E306",   # Expected 1 blank line before a nested definition(E306)
  "E502",   # The backslash is redundant between brackets(E502)
  "E704",   # Multiple statements on one line (def)(E704)
  "E901",   # SyntaxError or IndentationError(E901)
  "W391",   # Blank line at end of file(W391)
  "W504",   # Line break occurred after a binary operator(W504)
  "W601",   # .has_key() is deprecated, use 'in'(W601)
  "W602",   # Deprecated form of raising exception(W602)
  "W603",   # <>' is deprecated, use '!='(W603)
  "W604",   # Backticks are deprecated, use 'repr()'(W604)
  "F812",   # List comprehension redefines name from line n(F812)
  "F831",   # Duplicate argument name in function definition(F831)
]

Ruff 導入前後の速度比較

Ruff の導入前後の速度の比較結果がこちらです。time コマンドで計測しました。

flake8 + isort + black の処理にかかっていた時間と比較すると、 Ruff(ruff + ruff-format) ではなんと 1/15 以下になっています。

まとめ

以上、Ruff に関して紹介をさせていただきました。

Flake8 でのチェックを完全に Ruffに置き換えることはできなかったものの、CI環境ではFlake8、isort、Blackを脱却し、Ruff へ集約することができ、さらに15倍以上も処理が速くなりました。 今後のアップデートで、Flake8 から完全に脱却できることを期待しています。

Ruff の導入はマニュアルやツールが充実していて簡単に実施できたので、まずは Linter として、ぜひ試してみてください。 また、Ruffにはコントリビューションガイドがあります。Rustに興味がある方はこの機会にコントリビュートにトライしてみるのもありかもしれません。

最後に

最後まで記事をご覧いただき、ありがとうございます。

本記事では、Linter & Formatter として利用していた、 Flake8 + isort + Black から Ruff へ移行するために私が取り組んだ内容をご紹介しました。

この記事がみなさんのご活用に貢献できたら幸いです!

株式会社カケハシでは、一緒に開発していただけるエンジニアを募集しております。 興味がありましたら、ぜひ下記をご覧になってください!


  1. Apache Airflow、FastAPI、pandas、Polars、SciPy、Pydantic、Pylint...etc(公式サイトから引用してます)