🐍

Python向け爆速静的解析ツールtyを使ってみる

に公開

tyとは

Astral社が作っているRust製の爆速な型検査器および言語サーバーです。
https://github.com/astral-sh/ty

インストール

pypiで公開されているのでuv add すればOK。

uv add ty --dev

速度比較

比較方法

雑にHyperfineでベンチマークを取ってみる。

hyperfine 'uv run mypy --strict src' 'uv run ty check src --exit-zero'

なお、リポジトリは拙作のパッケージを使った。ついでに見てもらえると嬉しい。

https://github.com/shunsock/fukinotou

結果

mypyと比較するとキャッシュなしで約200倍キャッシュありで約4倍ほど高速になった。

mypyをキャッシュなしの状態からスタート

mypyをキャッシュありの状態からスタート

便利そうな機能

エラーが見やすい

Cargo (Rustのパッケージマネージャー) の体験を感じるエラーの出力がとてもよい。

error: lint:too-many-positional-arguments: Too many positional arguments to bound method `__init__`: expected 0, got 1
  --> src/fukinotou/abstraction/dataframe_exportable.py:65:31
   |
63 |             return pandas.DataFrame()
64 |
65 |         df = pandas.DataFrame(self._to_dicts(include_path_as_column))
   |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 |
67 |         return df
   |
info: `lint:too-many-positional-arguments` is enabled by default

Found 1 diagnostic

オプションがありがたい

  -W, --watch
          Watch files for changes and recheck files related to the changed files

cargo watch が Pythonでもできる日がくるとは...!! 実際この速度ならwatch機能使いたくなりますよね。次のようにすれば実用レベルのスピードで動きます。

uv run ty check src -W

ぱっと見でもこの辺りのオプションが便利そう。

--error-on-warning
  Use exit code 1 if there are any warning-level diagnostics

--exit-zero
  Always use exit code 0, even when there are error-level diagnostics

--output-format <OUTPUT_FORMAT>
  The format to use for printing diagnostic messages

  Possible values:
  - full:    Print diagnostics verbosely, with context and helpful hints
  - concise: Print diagnostics concisely, one per line

--python-version <VERSION>
  Python version to assume when resolving types

  [possible values: 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13]

これからに期待

一方で、Lintのエラーの理由はよくわからなかった。リポジトリに記載がある通り、正式なリリースまでは職場に持ち込むのは待った方がよさそう。

エラーを調査する人の図

fukinotou (main*) » uv run ty check src                                           ~/hobby/fukinotou
error: lint:too-many-positional-arguments: Too many positional arguments to bound method `__init__`: expected 0, got 1
  --> src/fukinotou/abstraction/dataframe_exportable.py:65:31
   |
63 |             return pandas.DataFrame()
64 |
65 |         df = pandas.DataFrame(self._to_dicts(include_path_as_column))
   |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 |
67 |         return df
   |
info: `lint:too-many-positional-arguments` is enabled by default

Found 1 diagnostic

このエラーが気になったので ty を開発している ruff のリポジトリで全文検索を掛けてみた。(repo:astral-sh/ruff too-many-positional-arguments)

読んでみたところ、「__init__に渡す引数が多すぎるよ」という内容のようだった。実際、検索結果には次のようなテストがあった。

# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
cast(str, b"ar", "foo")

https://github.com/astral-sh/ruff/blob/6cd8a49638fb12f4e7984c5c5de0469b1248f9f1/crates/ty_python_semantic/resources/mdtest/directives/cast.md?plain=1#L26

__init__ なので、pandas.DataFrame かなと思いpandasのリポジトリで引数を確認した。

def __init__(
    self,
    data=None,
    index: Axes | None = None,
    columns: Axes | None = None,
    dtype: Dtype | None = None,
    copy: bool | None = None,
) -> None:

https://github.com/pandas-dev/pandas/blob/0691c5cf90477d3503834d983f69350f250a6ff7/pandas/core/frame.py#L694

そういわれると気になってきたので df = pandas.DataFrame(data=self._to_dicts(include_path_as_column)) に替えてみたくなるものである。試したところ、エラーが次に変化した。

fukinotou (main*) » uv run ty check src                                        ~/hobby/fukinotou 1 ↵
error: lint:unknown-argument: Argument `data` does not match any known parameter of bound method `__init__`
  --> src/fukinotou/abstraction/dataframe_exportable.py:65:31
   |
63 |             return pandas.DataFrame()
64 |
65 |         df = pandas.DataFrame(data=self._to_dicts(include_path_as_column))
   |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 |
67 |         return df
   |
info: `lint:unknown-argument` is enabled by default

Found 1 diagnostic

もしかしたら、私が見落としていて、事前のセットアップが必要だった可能性はある。求む有識者。

Discussion