そぬばこ

備忘録とか、多分そんな感じ。

Python のフォーマッタ、ちょっと拡大して静的リントツールやパッケージ管理ツールに関する雑談

ただ思ったことを書き連ねています。 特に深い意味はない話です。

black と isort の微妙な違い

black は Python の著名なフォーマッタで、 isort は特に import 周りのソートを行う著名なフォーマッタです。

github.com

github.com

もちろん、役割の範囲が異なるので、そもそも違いとはなんぞやと感じる方のほうが多いと思います。 一応機能の話をしておくと、 black の機能が isort と競合する部分があるため、機能的な共存は isort 側に以下のような設定を入れることで可能になっています。

[tool.isort]
profile = "black"

個人的な微妙な違いへの興味は、 --diff オプションで得られる出力の性格にあります。 次のコードに対する、black と isort の結果の違いを見てみましょう

import typing
import os


print( "1" )

まずは black から。フォーマットがかからないように --check をつけています。

$ black --check --diff tmp.py
--- tmp.py  2024-02-27 16:05:01.986972+00:00
+++ tmp.py  2024-02-27 16:05:19.849258+00:00
@@ -1,6 +1,5 @@
 import typing
 import os


-print( "1" )
-
+print("1")
would reformat tmp.py

Oh no! 💥 💔 💥
1 file would be reformatted.

次に isort です。

$ isort --check --diff tmp.py
ERROR: /Users/nersonu/tmp/blog_202402/tmp.py Imports are incorrectly sorted and/or formatted.
--- /Users/nersonu/tmp/blog_202402/tmp.py:before    2024-02-28 01:05:01.986972
+++ /Users/nersonu/tmp/blog_202402/tmp.py:after 2024-02-28 01:05:30.731620
@@ -1,6 +1,5 @@
+import os
 import typing
-import os
-

 print( "1" )

似ていますがいろいろ微妙に異なっていますね。 1つずつ見ていきましょう。

1つ目は、メッセージの位置です。 black は最後に、isort は最初に「フォーマットかけるぜ」というメッセージが入っています。 また、これは主観的な感覚ですが、 isort のメッセージのほうがなんとなくシステマティックです。

# black
Oh no! 💥 💔 💥
1 file would be reformatted.
# isort
ERROR: /Users/nersonu/tmp/blog_202402/tmp.py Imports are incorrectly sorted and/or formatted.

2つ目は、フォーマット対象を示すファイルパスの表記の仕方です。 black は相対パスなのに対し、isort は絶対パスになっています。 さらに、 isort にはパスのあとに :before , :after の文字列があります。

# black
--- tmp.py
+++ tmp.py
# isort
--- /Users/nersonu/tmp/blog_202402/tmp.py:before
+++ /Users/nersonu/tmp/blog_202402/tmp.py:after

3つ目は、時間表記です。 black は UTC なのに対し、 isort はシステムの時間表記が使われています。 ちなみに TZ 環境変数を渡したとき、 black は依然として UTC ですが、 isort は指定したタイムゾーンになります。

# black
2024-02-27 16:05:01.986972+00:00
# isort
2024-02-28 01:05:01.986972

このように、ライブラリ2つ並べて出力を見てみるだけで文化の違いを感じることができます。 ところで、これらは恐らく diff コマンドの -u オプションから影響を受けていそうです。

$ diff -u tmp.py formatted_tmp.py
--- tmp.py  2024-02-28 02:14:11
+++ formatted_tmp.py    2024-02-28 02:15:20
@@ -1,5 +1,5 @@
-import typing
 import os
+import typing


-print( "1" )
+print("1")

OS や環境依存で diff の結果が変わる可能性もあるので、一概に確定的なことはここでは言いませんが、なんとなく、 black も isort も完全に仕様として一致しているわけではなさそうです。 不思議ですね。

些細な違いで困ること?

このような些細な違いで実際に困ることはあるのでしょうか? 例えばこの diff の結果を使うような状況が……?

ところで、みなさんは Reviewdog を知っていますか?

github.com

これは、何かのライブラリやツールでコード解析をした結果等を GitHub*1 で PR comment として出力できるようなツールです。

Reviewdog では、 解析結果のインプットのフォーマットとして、 diff のフォーマットに対応しています。 つまり、 black や isort の結果を使うことが出来ると考えられるわけですね。

ここで、タイムゾーン以外はほぼ diff のフォーマットの体裁を成している black は比較的簡単に使うことができます。 このノリで isort でやろうとすると痛い目を見るわけです。主にファイルパス周辺で。 実際に使うには出力を sed などで置換して消したりし、Reviewdog 側が解釈できるようにしてあげる必要があります。 あまり世で知れ渡っていないようですが、別言語でも Reviewdog でうまく扱えるように、シェルスクリプト上で出力を置換してあげるみたいなケースは他にあるようですね *2

black や isort に別れを告げる

ところで、ruff というツールをご存知でしょうか。

github.com

Rust 製の Python の静的リントツールですが、気づけばフォーマットも出来るようになっていました。

github.com

ruff についての説明は世に溢れているので省きますが、まだ開発途上でバグはありつつも Rust 製の非常に高速なツールです。 flake8 のような静的リントの置き換えだけでなく、black, isort のフォーマッタも ruff ですべて置き換わっていくことでしょう。

Python の開発ツールのこれから

ruff を開発している Astral 社は、同様に Rust で pip-tools の代替を目指す uv の開発も行っています。

github.com

Python の cargo を目指すというところで、 ruff を以前から採用し、 uv についても導入した rye ですが、最近作者が Astral 社にその開発を引き渡した *3 ことが話題になりました。

github.com

lucumr.pocoo.org

フォーマッタ、静的リント、パッケージ管理ツールと、基本的な開発ツールがどんどん rye に集約されていっており、まさに cargo を目標にしていることがはっきりわかりますね。 以前紹介した PDM や、 Poetry も依存解決に installer 等を使うようになってかなり高速化しましたが、これから rye が台頭してくると、まだまだ Python の開発ツールの戦乱期は終わりを迎えそうにはありません。

github.com

github.com

github.com

そんなことをぼんやりと考えながら、わたしの有給休暇の火曜日が終わっていきました。

*1:厳密には CLI だけでも使えるがここでは省略

*2:著者は友人と一度だけこの話をしました

*3:この表現は厳密には正しくないが、割愛。詳しくは記事を読んでほしい