CLOVER🍀

That was when it all began.

Rustで書かれたPython用のリンター、フォーマッターであるRuffを使う

これは、なにをしたくて書いたもの?

Pythonソースコードを書くにあたって、そろそろリンターやフォーマッターを適用しておきたいなという気がしまして。
特にPython慣れしていないので、リンターに指摘して欲しいみたいなところがあります。

最近はRuffというものがよさそうなので、こちらを導入してみます。設定にはこだわらないことにします。

Ruff

RuffのWebサイトはこちらです。

Ruff

GitHubリポジトリーはこちら。

GitHub - astral-sh/ruff: An extremely fast Python linter and code formatter, written in Rust.

RuffはRustで書かれたPython用のリンター、コードフォーマッターです。

An extremely fast Python linter and code formatter, written in Rust.

以下を特徴としているようです。

  • 既存のリンター(Flake8など)やフォーマッター(Blackなど)より10〜100倍高速
  • pipでインストール可能
  • pyproject.tomlのサポート
  • Python 3.13互換
  • 変更していないファイルの再解析を避けるため、キャッシュをビルトイン
  • 自動修正をサポート(不要なimportの自動削除など)
  • 800以上のルールを実装していて、ポピュラーなFlake8プラグインやflake8-bugbearなどをネイティブに再実装
  • Visual Studio Codeなどのファーストパーティーなエディターとのインテグレーションをサポート
  • 階層的かつカスケードな構成をサポートしており、モノレポフレンドリー

既存の以下のツール群を置き換えることができるようなのですが、自分は他のツールの事情は知りません…。どういうジャンルにどういう
ツールがあるのか?という意味で押さえておきます。

またPython 3.13互換ということでどのバージョンのPythonをサポートしているのか気になるところでしたが、FAQを見ると
3.7以上のようですね。

Ruff can lint code for any Python version from 3.7 onwards, including Python 3.13.

FAQ / What versions of Python does Ruff support?

今回は設定は凝らず、ひとまず導入してみたり設定方法を少し調べるところまでにしておきます。

環境

今回の環境はこちら。

$ uv --version
uv 0.5.14


$ python3 --version
Python 3.12.3

Ruffをインストールする

まずはuvプロジェクトを作成。

$ uv init --vcs none hello-ruff
$ cd hello-ruff

インストールはpip installで行う方法が紹介されています。

Installing Ruff | Ruff

FAQではuvを使った場合も載っていて、uv add --devuv tool installが紹介されています。
※FAQ自体はRustのインストールが必要か?ですが

FAQ / Do I need to install Rust to use Ruff?

今回はuv add --devを使うことにします。

$ uv add --dev ruff 

pyproject.toml

[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "ruff>=0.8.6",
]

インストールされたライブラリーの一覧。Ruff本体だけなんですね、良いです。

$ uv pip list
Package Version
------- -------
ruff    0.8.6

まずはRuffのインストールができました。

Ruffを使ってみる

さて、どうしましょうか。適当なソースコードが必要なので、チュートリアルのものを使うことにします。

Tutorial | Ruff

こういう構成にしました。

$ tree -P '*.py'
.
├── foo.py
└── numbers
    ├── __init__.py
    └── bar.py

2 directories, 3 files

2つのPythonファイルがありますが、中身は同じです。

foo.py

from typing import Iterable

import os


def sum_even_numbers(numbers: Iterable[int]) -> int:
    """Given an iterable of integers, return the sum of all even numbers in the iterable."""
    return sum(
        num for num in numbers
        if num % 2 == 0
    )

numbers/bar.py

from typing import Iterable

import os


def sum_even_numbers(numbers: Iterable[int]) -> int:
    """Given an iterable of integers, return the sum of all even numbers in the iterable."""
    return sum(
        num for num in numbers
        if num % 2 == 0
    )
リンターとして使う

Ruffをリンターとして使ってみましょう。

使い方はチュートリアルにも書かれていますが、

Tutorial | Ruff

こちらを見るのがよいでしょうね。

The Ruff Linter | Ruff

Ruffの設定はこうしました。[tool.ruff.lint]の部分がRuffのリンターの設定です。

pyproject.toml

[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "ruff>=0.8.6",
]

[tool.ruff]

[tool.ruff.lint]
select = [
    "E",  # pycodestyle error
    "W",  # pycodestyle warning
    "F",  # Pyflakes
    "UP",  # pyupgrade
    "B",  # flake8-bugbear
    "SIM",  # flake8-simplify
    "I",  # isort
]

[tool.ruff.format]

設定はpyproject.tomlruff.toml.ruff.tomlで行います。ruff.tomlまたは.ruff.tomlで書く場合は、pyproject.tomlにおける[tool]の部分が
なくなります。

記述方法はこちらを見るとよいでしょう。

Configuring Ruff | Ruff

RuffのリンターのデフォルトのルールはEFのようですが、今回は上記の範囲で有効にしました。

Ruff would enable all rules with the E (pycodestyle) or F (Pyflakes) prefix, with the exception of F401.

The Ruff Linter / Rule selection

このEとかFとかはRuffのルールのプリフィックスです。

Rules | Ruff

Ruff's linter mirrors Flake8's rule code system, in which each rule code consists of a one-to-three letter prefix, followed by three digits (e.g., F401). The prefix indicates that "source" of the rule (e.g., F for Pyflakes, E for pycodestyle, ANN for flake8-annotations).

The Ruff Linter / Rule selection

実行してみます。ruff checkでリンターを実行できます。

$ uv run ruff check
foo.py:1:1: UP035 [*] Import from `collections.abc` instead: `Iterable`
  |
1 | from typing import Iterable
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
2 |
3 | import os
  |
  = help: Import from `collections.abc`

foo.py:1:1: I001 [*] Import block is un-sorted or un-formatted
  |
1 | / from typing import Iterable
2 | |
3 | | import os
4 | |
5 | |
6 | | def sum_even_numbers(numbers: Iterable[int]) -> int:
  | |_^ I001
7 |       """Given an iterable of integers, return the sum of all even numbers in the iterable."""
8 |       return sum(
  |
  = help: Organize imports

foo.py:3:8: F401 [*] `os` imported but unused
  |
1 | from typing import Iterable
2 |
3 | import os
  |        ^^ F401
  |
  = help: Remove unused import: `os`

foo.py:7:89: E501 Line too long (92 > 88)
  |
6 | def sum_even_numbers(numbers: Iterable[int]) -> int:
7 |     """Given an iterable of integers, return the sum of all even numbers in the iterable."""
  |                                                                                         ^^^^ E501
8 |     return sum(
9 |         num for num in numbers
  |

numbers/bar.py:1:1: UP035 [*] Import from `collections.abc` instead: `Iterable`
  |
1 | from typing import Iterable
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
2 |
3 | import os
  |
  = help: Import from `collections.abc`

numbers/bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted
  |
1 | / from typing import Iterable
2 | |
3 | | import os
4 | |
5 | |
6 | | def sum_even_numbers(numbers: Iterable[int]) -> int:
  | |_^ I001
7 |       """Given an iterable of integers, return the sum of all even numbers in the iterable."""
8 |       return sum(
  |
  = help: Organize imports

numbers/bar.py:3:8: F401 [*] `os` imported but unused
  |
1 | from typing import Iterable
2 |
3 | import os
  |        ^^ F401
  |
  = help: Remove unused import: `os`

numbers/bar.py:7:89: E501 Line too long (92 > 88)
  |
6 | def sum_even_numbers(numbers: Iterable[int]) -> int:
7 |     """Given an iterable of integers, return the sum of all even numbers in the iterable."""
  |                                                                                         ^^^^ E501
8 |     return sum(
9 |         num for num in numbers
  |

Found 8 errors.
[*] 6 fixable with the `--fix` option.

カレントディレクトリー配下を、サブディレクトリー含めて見てくれるようですね。

自動で修正できるものについては、--fixオプションをつけることで修正できるようです。

$ uv run ruff check --fix
foo.py:5:89: E501 Line too long (92 > 88)
  |
4 | def sum_even_numbers(numbers: Iterable[int]) -> int:
5 |     """Given an iterable of integers, return the sum of all even numbers in the iterable."""
  |                                                                                         ^^^^ E501
6 |     return sum(
7 |         num for num in numbers
  |

numbers/bar.py:5:89: E501 Line too long (92 > 88)
  |
4 | def sum_even_numbers(numbers: Iterable[int]) -> int:
5 |     """Given an iterable of integers, return the sum of all even numbers in the iterable."""
  |                                                                                         ^^^^ E501
6 |     return sum(
7 |         num for num in numbers
  |

Found 2 errors.

8個エラーになっていたものが、2個まで減りました。

残っているのはdocstringの長さオーバーですが、これは今回はそのままにしておきます。

修正後のファイルはこうなっていました。

foo.py

from collections.abc import Iterable


def sum_even_numbers(numbers: Iterable[int]) -> int:
    """Given an iterable of integers, return the sum of all even numbers in the iterable."""
    return sum(
        num for num in numbers
        if num % 2 == 0
    )

リンターの設定はこちらおよびルールを見ながら設定することになります。

Settings / lint

Rules | Ruff

また、ここのリンターに対して以下のように[tool.ruff.lint.xxxxx]として設定できることを覚えておいた方がよさそうですね。

[tool.ruff.lint.flake8-bugbear]
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
フォーマッターとして使う

最後にRuffをフォーマッターとして使ってみます。

やはり使い方はチュートリアルにも書かれていますが、

Tutorial | Ruff

こちらを見るのがよいでしょうね。

The Ruff Formatter | Ruff

実行はruff formatなのですがファイルが直接変わってしまうので、いきなり実行するのではなく--checkまたは--diffオプションで
チェックと差分を見てみましょう。

$ uv run ruff format --check
$ uv run ruff format --diff

結果はそれぞれこうなりました。

$ uv run ruff format --check
Would reformat: foo.py
Would reformat: numbers/bar.py
2 files would be reformatted, 1 file already formatted


$ uv run ruff format --diff
--- foo.py
+++ foo.py
@@ -3,7 +3,4 @@

 def sum_even_numbers(numbers: Iterable[int]) -> int:
     """Given an iterable of integers, return the sum of all even numbers in the iterable."""
-    return sum(
-        num for num in numbers
-        if num % 2 == 0
-    )
+    return sum(num for num in numbers if num % 2 == 0)

--- numbers/bar.py
+++ numbers/bar.py
@@ -3,7 +3,4 @@

 def sum_even_numbers(numbers: Iterable[int]) -> int:
     """Given an iterable of integers, return the sum of all even numbers in the iterable."""
-    return sum(
-        num for num in numbers
-        if num % 2 == 0
-    )
+    return sum(num for num in numbers if num % 2 == 0)

2 files would be reformatted, 1 file already formatted

--checkはファイルをフォーマットせず、チェックだけ行います。これで、どのファイルがフォーマットされるのかを確認できます。
-diffもファイルをフォーマットせずチェックを行いますが、合わせて変更内容を表示してくれます。

フォーマットする時にはこれらのオプションを外します。

$ uv run ruff format

フォーマッターを実行した結果。

foo.py

from collections.abc import Iterable


def sum_even_numbers(numbers: Iterable[int]) -> int:
    """Given an iterable of integers, return the sum of all even numbers in the iterable."""
    return sum(num for num in numbers if num % 2 == 0)

フォーマッターに関する設定はこちら。

Settings / format

今回は特に設定したいものがなかったので、[tool.ruff.format]という枠だけにしました。

pyproject.toml

[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "ruff>=0.8.6",
]

[tool.ruff]

[tool.ruff.lint]
select = [
    "E",  # pycodestyle error
    "W",  # pycodestyle warning
    "F",  # Pyflakes
    "UP",  # pyupgrade
    "B",  # flake8-bugbear
    "SIM",  # flake8-simplify
    "I",  # isort
]

[tool.ruff.format]

フォーマッターに関しては、Settingsに記載されている内容以上の設定はできないようです。

Given the focus on Black compatibility (and unlike formatters like YAPF), Ruff does not currently expose any other configuration options.

The Ruff Formatter / Configuration

pyproject.tomlの設定

ベースにしていたpyproject.tomlの設定を載せておきます。使っていって気が変わったら、こちらの設定内容を変更していくかもしれません。

pyproject.toml

[project]
name = "hello-ruff"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[dependency-groups]
dev = [
    "ruff>=0.8.6",
]

[tool.ruff]

[tool.ruff.lint]
select = [
    "E",  # pycodestyle error
    "W",  # pycodestyle warning
    "F",  # Pyflakes
    "UP",  # pyupgrade
    "B",  # flake8-bugbear
    "SIM",  # flake8-simplify
    "I",  # isort
]

[tool.ruff.format]

おわりに

Rustで書かれたPython用のリンター、フォーマッターであるRuffを使ってみました。

Pythonでの他のリンター、フォーマッターを使ったことがないので比較ができないのですが、ひとまずこれで慣れていこうと思います。