🪦

pytypeは死んだもういないーPythonの型チェッカーについて

に公開

はじめに

「バグを回避するために型による静的解析を検討する」というのはEffective Pythonにも書かれているとおり最近はPythonの型の静的解析をサポートするツールが増えています。

Googleのpytypeは2012年から長年、型注釈の付いていコードに対しての型に対する静的解析として有力な候補でした。
しかしながら、バイトコードを解析してチェックするという作りの問題上、Pythonの新機能の追従に問題をかかえ、Python 3.12 を最後に新バージョン対応を終了するとの発表がなされました。

本ドキュメントでは、その代替となるツールの比較調査を行います。

Pythonの型の注釈について

基本的な書き方

以下の例のように、型に対する注釈を記述できます。

型ヒントを使用した実装例

from asyncio import sleep
from collections.abc import Generator

def func_1(x: int) -> None:
    assert x == 1

def func_2(x: int, y: str) -> tuple[int, str]:
    return (x, y)

# |で複合型
def func_3(x: int, y: str) -> list[str | int]:
    return [
        x,
        y
    ]

def func_4(x: int, y: str) -> dict[str, int]:
    return {
        y: x
    }

class A:
    def __init__(self, x: int) -> None:
        self.x = x

def func_5(x:int) -> A:
    a = A(x)
    return a

# asyncの場合はreturnで返す型を記載する
async def func_6(x: int)->int:
    await sleep(1)
    return x + 1

def func_7()->Generator[int, str, str]:
    print('1......')
    r1 = yield 1
    print('.....', r1)
    print('2......')
    r2 = yield 2
    print('.....', r2)
    yield 3
    return "ok"

def func_8()->Generator[int, str, str]:
    ret = yield from func_7()   # func_7 の return 値がここに入る
    print("got:", ret) 
    return ret

def func_9()->None:
    it = func_8()
    print('start...')
    print("a", next(it))
    print("b", it.send("test1"))
    print("c", it.send("test2"))
    try:
        print(next(it))
    except StopIteration as e:
        print("return =", e.value)   # => return = ok


class FirstClass:
    # 型ヒントとしてリテラルを使用できる
    def __init__(self, value: "SecondClass") -> None:
        self.value = value

class SecondClass:
    def __init__(self, value: int) -> None:
        self.value = value

ここで記載した型にたいするヒントはmypyなどの型チェッカーによってのみチェックされ、Pythonの実行時に影響を与えることはありません。
詳しい書き方については後述の参考資料を確認してください。

型チェッカーが型をどう推論したかを検証する

reveal_typeを使用することで型チェッカーが推論した型を表示することができます。
ほとんどの型チェッカーはreveal_type()をインポートなしで使用できます。(実行時にはimportが必須です)

例:

from typing import TYPE_CHECKING, reveal_type

x = 1
reveal_type(x)

# 型チェッカーだけで表示したい場合
if TYPE_CHECKING:
    reveal_type(x)

mypyの出力例
reveal_typeで指定した変数について型チェッカーがどう解釈したかを出力します

% pipenv run mypy src/type013.py
src/type013.py:4: note: Revealed type is "builtins.int"
src/type013.py:8: note: Revealed type is "builtins.int"

コードを実行した場合
Runtimeでの型を出力します。
もし、型チェッカー以外で表示したくないときはTYPE_CHECKINGを使用することで出力を抑制できます。

% pipenv run python src/type013.py
Runtime type is 'int'

参考資料

代替ツール

今回,pytypeの代わりに実験したツールは以下のとおりです。

mypy

mypyも2012年には開発の始まっている歴史あるツールです。

Google Trendsによると、いくつかの代替ツールと比較しても検索の関心が高いことがわかります。

また、2025/09時点においても活発に開発がなされていることが確認できます。

https://github.com/python/mypy/pulse/monthly

mypyは基本的に型注釈があるコードのチェックを行うことが目的になっています。

たとえば以下のような型注釈があるコードについて型の不整合は検出ができます。

def func()->str:
    1 / "test" # 明らかにエラー
    return "test"

x = func()
x / 2 # 明らかにエラー

% pipenv run mypy src/type012.py  
src/type012.py:3: error: Unsupported operand types for / ("int" and "str")  [operator]
Found 1 error in 1 file (checked 1 source file)
itagakimasaki@macpro pipenv310 % pipenv run mypy src/type012.py --check-untyped-defs
src/type012.py:3: error: Unsupported operand types for / ("int" and "str")  [operator]
Found 1 error in 1 file (checked 1 source file)

しかし以下のような型注釈を省略してあるコードの型の不整合は検出できません。

def func():
    1 / "test" # 明らかにエラー
    return "test"

x = func()
x / 2 # 明らかにエラー
% pipenv run mypy src/type012.py         
Success: no issues found in 1 source file

デフォルトではどちらのエラーも検出されません。
これはmypyのデフォルトの挙動では型注釈のない関数のチェックをスキップするからです。

--check-untyped-defsを使用することで型注釈のない関数のチェックも可能になります。

 % pipenv run mypy src/type012.py --check-untyped-defs
src/type012.py:2: error: Unsupported operand types for / ("int" and "str")  [operator]
Found 1 error in 1 file (checked 1 source file)

このケースでは関数内の不整合は検出しましたが、xをanyとみなすため、関数の戻り値の型の不整合は検出できません。
--strictオプションなどを使用して厳密なチェックなどをするオプションがありますが、そこで検出されるエラーは型ヒントが記載されていないことを注意するものになります。

% pipenv run mypy src/type012.py --strict            
src/type012.py:1: error: Function is missing a return type annotation  [no-untyped-def]
src/type012.py:2: error: Unsupported operand types for / ("int" and "str")  [operator]
src/type012.py:5: error: Call to untyped function "func" in typed context  [no-untyped-call]
Found 3 errors in 1 file (checked 1 source file)

そのため、新規のプロジェクトにおいては導入しやすいですが、型注釈のない既存のプロジェクトに導入した場合、その効果は限定的になります。

Pyright

pyrightはMicrosoftの開発した型チェッカーです。
Google Trendsによるとmypyについて検索の関心があります。

また、2025/09時点においても活発に開発がなされていることが確認できます。

https://github.com/microsoft/pyright/pulse/monthly

pyrightとmypyの違いについては以下にドキュメントがあります。
Differences Between Pyright and Mypy

Pyrightの最も大きな強みはPythonの公式の型付けの仕様に最も準拠していることです。

現時点の各ツールがPythonの公式の型付にどの程度、準拠しているかは下記を参考にしてください。
https://htmlpreview.github.io/?https://github.com/python/typing/blob/main/conformance/results/results.html

mypyとの違いで特筆するべき点は2点です。

  • mypyより高いパフォーマンスを目指す
  • 注釈のないコードのチェックも行う

mypyよりも高いパフォーマンスを目標としていることは大規模なPythonのプロジェクトにおける開発体験に直結します。

また、型注釈を省略してあるケースについてもある程度のチェックをしてくれることは型注釈のないコードを抱えている環境では有用でしょう。

以下は型注釈がない場合のチェックの例になります。
xを"Literal['test']"とみなしてx/2をエラーとします。

def func():
    1 / "test" # 明らかにエラー
    return "test"

x = func()
x / 2 # 明らかにエラー

% pipenv run pyright src/type012.py 
/work/techblog/python_perfomance/pipenv310/src/type012.py
  /work/techblog/python_perfomance/pipenv310/src/type012.py:2:5 - error: Operator "/" not supported for types "Literal[1]" and "Literal['test']" (reportOperatorIssue)
  /work/techblog/python_perfomance/pipenv310/src/type012.py:2:5 - warning: Expression value is unused (reportUnusedExpression)
  /work/techblog/python_perfomance/pipenv310/src/type012.py:6:1 - error: Operator "/" not supported for types "Literal['test']" and "Literal[2]" (reportOperatorIssue)
  /work/techblog/python_perfomance/pipenv310/src/type012.py:6:1 - warning: Expression value is unused (reportUnusedExpression)
2 errors, 2 warnings, 0 informations

Pyrefly

Pyreflypyreの後継としてMetaが開発中のRustベースの静的な型チェッカーです。
2025年末までに既存のツールに置き換える予定になっています。

2025年9月時点で活発に活動がされています。

https://github.com/facebook/pyrefly/pulse/monthly
プルリクエストのマージの数は少ないですが、コードの変更量はmypyやpyrightを上回っています。

Pyreflyの最も大きな特徴は、そのパフォーマンスです。
Meta の公開ベンチマークでは、PyTorchのコードに対する解析時間がPyrightと比較しても10倍以上はやいと報告されています。

https://pyrefly.org/

また、mypy,pyrightからの移行を容易にするためのドキュメントが用意されいています。

また、型注釈を自動で追加する機能も用意されているので大量な既存コードを抱える場合の選択肢として大きなアドバンテージでしょう。

既存コード:

def func():
    1 / "test" # 明らかにエラー
    return "test"

x = func()
x / 2 # 明らかにエラー

コマンド:

% pipenv run pyrefly infer src/type012.py 
 INFO Checking project configured at `/Users/itagakimasaki/work/techblog/python_perfomance/pipenv310/pyrefly.toml`
 INFO 2 errors

変更後

def func() -> str:
    1 / "test" # 明らかにエラー
    return "test"

x = func()
x / 2 # 明らかにエラー

たいへん、魅力的なツールですが、繰り返すように現在開発中であることを留意してください。

ty

tyはAstralによって開発中のRustベースの型チェッカーです。
現時点ではプロダクトコードに対する適用は避けた方が望ましいでしょう。

2025年9月時点での活動状況は以下の通りです。

https://github.com/astral-sh/ty/pulse/monthly

以下のブログで記述がありますが、Pyreflyよりさらに高速なツールです。

Pyrefly vs. ty: Comparing Python’s Two New Rust-Based Type Checkers

また、このブログは動作についての比較もあるのでtyを検討する際は一読をお勧めします。

また、2025年9月時点で実験した範囲では、いくらか他の型チェッカーとの差異がありました。

  • reveal_typeをインポートしないで使うと「undefined-reveal」となる
  • 仕様なのか不具合なのかわからない挙動がある(2025/09 時点の観測)

例1:xがUnkonwnと認識されてエラーにならない

from typing import reveal_type

def func():
    return "test"

x = func()
reveal_type(x) # 型を確認

x / 2 # 明らかにエラー

PyrightやPyreflyは xをLiteral['test']とみなしますが、tyはunkownとみなしてエラーとしません

例2:型注釈のないlist

from typing import reveal_type

l = [1,2]
reveal_type(l) # 型を確認
l.append("test")

PyrightやPyreflyは lをlist[int]とみなしますが、tyはlist[Unknown]とみなしてエラーとしません。

まとめ

おそらく、どのツールを採用すべきかは状況によってことなるでしょう。

実績やより多くの人間が使っているという点を考慮すればmypyになりますし、型注釈がないコードを大量に抱えている場合は違う選択肢になるでしょう。
パフォーマンスを最重視する場合は現時点で開発中であるというリスクを考慮した上でRustベースのPyreflyかtyが候補になります。

ただ、チェック結果についてはtyとPyreflyの出力結果がもっともわかりやすく感じました。

Discussion