CLOVER🍀

That was when it all began.

Pythonの静的型チェッカー、Pyrightを試す

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

以前、Pythonでの型ヒントとMypyについて調べてみました。

Pythonで型ヒント(Type Hints)を試してみる(+Mypy) - CLOVER🍀

もうひとつ押さえておいた方がよさそうな型チェッカーとしてPyrightがあるようなので、こちらも試してみましょう。

Pyright

PyrightのWebサイトはこちら。

Pyright

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

GitHub - microsoft/pyright: Static Type Checker for Python

Pyrightを使うこと自体は初めてではなく、PythonのLSP ServerとしてEmacsから使っています。

Emacsにlsp-mode+Python Language Serverをインストールする - CLOVER🍀

この時にインストールしたのがPyrightです。

Pyrightの静的型チェックに関する情報は、以下を見るのがよさそうです。

型の定義にはスタブファイルも使うようですね。

Type Stub Files

提供形態はいくつかあります。

Installation

自分が以前インストールしたのは、Language ServerとしてのPyrightをEmacsで使う方法ということになります。
またCLIとしてはnpmでインストールしています。

Pythonプロジェクトに導入することが多いと思いますので、今回はPyPiパッケージとして使ってみましょう。

またMypyとの違いも気になるところですね。こちらに書かれています。

Differences Between Pyright and Mypy

各型チェッカーの実行結果はこちら。

Python Type System Conformance Test Results

Mypyとの動作や推論の違いについても書かれていますが、それ以外の違いとしては以下も大きいでしょう。

  • Pyrightの方がMypyよりも高速なことが多い
  • PyrightはLanguage Serverの基盤になっている
  • Mypyのようなプラグイン機構はPyrightにはない

環境

今回の環境はこちら。

$ python3 --version
Python 3.12.3


$ uv --version
uv 0.9.5

準備

まずはuvプロジェクトを作成して準備します。

$ uv init --vcs none pyright-getting-started
$ cd pyright-getting-started
$ rm main.py

生成されたPythonスクリプトは削除。

プロジェクトにはRuffくらいを追加しておきます。

$ uv add --dev ruff

Pyrightを使ってみる

それではPyrightを使ってみましょう。

開発用パッケージとしてPyrightを追加。

$ uv add --dev pyright

pyproject.tomlはこのようになりました。

pyproject.toml

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

[dependency-groups]
dev = [
    "pyright>=1.1.407",
    "ruff>=0.14.2",
]

インストールされた依存関係。

$ uv pip list
Package           Version
----------------- -------
nodeenv           1.9.1
pyright           1.1.407
ruff              0.14.2
typing-extensions 4.15.0

バージョンとヘルプ。

$ uv run pyright --version
pyright 1.1.407


$ uv run pyright --help
Usage: pyright [options] files...
  Options:
  --createstub <IMPORT>              Create type stub file(s) for import
  --dependencies                     Emit import dependency information
  -h,--help                          Show this help message
  --ignoreexternal                   Ignore external imports for --verifytypes
  --level <LEVEL>                    Minimum diagnostic level (error or warning)
  --outputjson                       Output results in JSON format
  -p,--project <FILE OR DIRECTORY>   Use the configuration file at this location
  --pythonplatform <PLATFORM>        Analyze for a specific platform (Darwin, Linux, Windows)
  --pythonpath <FILE>                Path to the Python interpreter
  --pythonversion <VERSION>          Analyze for a specific version (3.3, 3.4, etc.)
  --skipunannotated                  Skip analysis of functions with no type annotations
  --stats                            Print detailed performance stats
  -t,--typeshedpath <DIRECTORY>      Use typeshed type stubs at this location
  --threads <optional COUNT>         Use separate threads to parallelize type checking
  -v,--venvpath <DIRECTORY>          Directory that contains virtual environments
  --verbose                          Emit verbose diagnostics
  --verifytypes <PACKAGE>            Verify type completeness of a py.typed package
  --version                          Print Pyright version and exit
  --warnings                         Use exit code of 1 if warnings are reported
  -w,--watch                         Continue to run and watch for changes
  -                                  Read files from stdin

では使ってみましょう。試しに関数に定義した引数の型と、実際の呼び出し時に指定している型が異なるスクリプトを用意。

bad_type.py

def add(a: int, b: int) -> int:
    return a + b

print(add("foo", "bar"))

実行。

$ uv run pyright
/path/to/pyright-getting-started/bad_type.py
  /path/to/pyright-getting-started/bad_type.py:4:11 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "a" of type "int" in function "add"
    "Literal['foo']" is not assignable to "int" (reportArgumentType)
  /path/to/pyright-getting-started/bad_type.py:4:18 - error: Argument of type "Literal['bar']" cannot be assigned to parameter "b" of type "int" in function "add"
    "Literal['bar']" is not assignable to "int" (reportArgumentType)
2 errors, 0 warnings, 0 informations

intの引数には割り当てられないと怒られましたね。

では、型宣言のない関数を追加してみます。

bad_type.py

def add(a: int, b: int) -> int:
    return a + b

print(add("foo", "bar"))

def message(s):
    print(s)

実行すると、特にエラーが増えませんでした。

$ uv run pyright
/path/to/pyright-getting-started/bad_type.py
  /path/to/pyright-getting-started/bad_type.py:4:11 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "a" of type "int" in function "add"
    "Literal['foo']" is not assignable to "int" (reportArgumentType)
  /path/to/pyright-getting-started/bad_type.py:4:18 - error: Argument of type "Literal['bar']" cannot be assigned to parameter "b" of type "int" in function "add"
    "Literal['bar']" is not assignable to "int" (reportArgumentType)
2 errors, 0 warnings, 0 informations

デフォルトのチェックルールには含まれていないようですね。ここで設定を確認してみます。

Pyright Configuration

PyrightではtypeCheckingModeでルールセットを指定できるようです。offbasicstandardstrictの4つから指定でき、デフォルトでは
standardになっているようです。

Pyright Configuration / Type Check Diagnostics Settings

設定はpyrightconfig.jsonまたはpyproject.tomlに記述します。

今回はpyproject.tomlに記述しましょう。

pyproject.toml

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

[dependency-groups]
dev = [
    "pyright>=1.1.407",
    "ruff>=0.14.2",
]

[tool.pyright]
typeCheckingMode = "strict"

strictにしました。

再実行。

$ uv run pyright
/path/to/pyright-getting-started/bad_type.py
  /path/to/pyright-getting-started/bad_type.py:4:11 - error: Argument of type "Literal['foo']" cannot be assigned to parameter "a" of type "int" in function "add"
    "Literal['foo']" is not assignable to "int" (reportArgumentType)
  /path/to/pyright-getting-started/bad_type.py:4:18 - error: Argument of type "Literal['bar']" cannot be assigned to parameter "b" of type "int" in function "add"
    "Literal['bar']" is not assignable to "int" (reportArgumentType)
  /path/to/pyright-getting-started/bad_type.py:6:13 - error: Type of parameter "s" is unknown (reportUnknownParameterType)
  /path/to/pyright-getting-started/bad_type.py:6:13 - error: Type annotation is missing for parameter "s" (reportMissingParameterType)
  /path/to/pyright-getting-started/bad_type.py:7:11 - error: Argument type is unknown
    Argument corresponds to parameter "values" in function "print" (reportUnknownArgumentType)
5 errors, 0 warnings, 0 informations

今度はエラーになりました。

typeCheckingModeで指定できる値と、指定に応じてどのルールがどのような設定になるのかは、以下で確認できるようです。

Pyright Configuration / Diagnostic Settings Defaults

strictにしたからといって、すべてのルールが有効になるわけではなさそうですね。

typeCheckingModeを設定してから、個別のルールを必要に応じてオーバーライドするのがよさそうです。

Pyright Configuration / Type Check Diagnostics Settings / Type Check Rule Overrides

strictで有効になっていないものとPythonのバージョン指定を含めて、こんな感じではないでしょうか。

[tool.pyright]
pythonVersion = "3.12"

typeCheckingMode = "strict"

deprecateTypingAliases = true
reportImplicitOverride = "erorr"
reportImplicitStringConcatenation = "error"
reportImportCycles = "error"
reportMissingSuperCall = "error"
reportPropertyTypeMismatch = "error"
reportUnnecessaryTypeIgnoreComment = "error"
reportUnreachable = "error"

これで少し試してみましょう。

おわりに

Pythonの静的型チェッカー、Pyrightを試してみました。

Mypyよりも高速ということなのですが、今回はその差を実感するような確認はしていません。

実行形態や設定方法などもなんとなくわかったので、ちょっと使っていってみましょう。