🦄

PythonでNewTypeの利用とリンター動作の確認

2025/01/07に公開

モチベーション

  • PythonでTypeScritやGoのようなPrimitive Typesを利用してアプリケーションを堅牢にしたい。同時に厳密な型のチェックをリンターで実施したい。
  • Pythonで型チェックするライブラリは数種類あるがどれがベストなのか把握できない。

検証コード

from typing import NewType

UserId = NewType('UserId', int) # 独自型 UserIdを指定

def get_user_name(user_id: UserId) -> str: # 引数にUserIdを指定
    return f"User: {user_id}"

def main() -> None:
    user_name=get_user_name(1) # int型がセットされている。
    print(user_name)

if __name__ == "__main__":
    main()

UserId = NewType('UserId', int)で独自の型を作成してコード内で利用しています。
コード内で型が誤っているはずですが、処理は成功してしまいます。

$ python new-type.py 
User: 1

Linterによるチェック

この状態で、各リンターライブラリでPrimitive Typeの不一致が検出できるのかを検証しました。
結果は以下となりました。

ライブラリ Primitive Typeの不一致検出
Ruff NG
Mypy OK
Mypy(strict) OK
Pyright OK
pytype OK

Ruff

NG:検知ができなかった。

$ ruff check         
All checks passed!

Mypy

OK:型の不一致を検知

$ mypy new-type.py  
new-type.py:9: error: Argument 1 to "get_user_name" has incompatible type "int"; expected "UserId"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Mypy strict mode

OK:型の不一致を検知

$ mypy --strict new-type.py  
new-type.py:9: error: Argument 1 to "get_user_name" has incompatible type "int"; expected "UserId"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

Pyright

OK:型の不一致を検知

$ Pyright new-type.py
Pyproject file parse attempt 1 error: {}
Pyproject file parse attempt 2 error: {}
Pyproject file parse attempt 3 error: {}
Pyproject file parse attempt 4 error: {}
Pyproject file parse attempt 5 error: {}
Pyproject file parse attempt 6 error: {}
Config file "/Users/takeshiiijima/github/python-typecheck/pyproject.toml" could not be parsed. Verify that format is correct.
/Users/takeshiiijima/github/python-typecheck/new-type.py
  /Users/takeshiiijima/github/python-typecheck/new-type.py:9:29 - error: Argument of type "Literal[1]" cannot be assigned to parameter "user_id" of type "UserId" in function "get_user_name"
    "Literal[1]" is not assignable to "UserId" (reportArgumentType)
1 error, 0 warnings, 0 informations 

Pytype

OK:型の不一致を検知

$pytype new-type.py
Computing dependencies
Analyzing 1 sources with 0 local dependencies
ninja: Entering directory `.pytype'
[1/1] check test
FAILED: /Users/takeshiiijima/Desktop/test/.pytype/pyi/test.pyi 
/Users/takeshiiijima/Library/Caches/pypoetry/virtualenvs/test-XsGIjM_l-py3.10/bin/python -m pytype.main --imports_info /Users/takeshiiijima/Desktop/test/.pytype/imports/test.imports --module-name test --platform darwin -V 3.10 -o /Users/takeshiiijima/Desktop/test/.pytype/pyi/test.pyi --analyze-annotated --nofail --quick /Users/takeshiiijima/Desktop/test/test.py
/Users/takeshiiijima/Desktop/test/test.py:9:1: error: in main: Function get_user_name was called with the wrong arguments [wrong-arg-types]
         Expected: (user_id: UserId)
  Actually passed: (user_id: int)

    user_name=get_user_name(1)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    user_name=get_user_name(1)



For more details, see https://google.github.io/pytype/errors.html#wrong-arg-types
ninja: build stopped: subcommand failed.
Leaving directory '.pytype'

Python Type System Conformance Test Results

ライブラリごとの対応表、Pyrightが全体を網羅しているように見えます。

参考記事

Discussion