🕌

mypyでPythonの型チェックをしてみた

に公開

今回はPythonの型チェックをできるmypyを使ってみたので紹介します。

mypyとは?

mypyはPythonの静的型チェックツールになります。Pythonは動的型付言語であり、データ型の厳密なチェックが基本的にされません。実装の難易度を下げて柔軟なコーディングができる反面、データ型の間違いによるエラーが発生するかどうかが実行するまでわからないという弱点があります。mypyを使うことにより、静的型チェックを実施することができるので、データ型の扱い方のミスなどを事前に気づくことができます。

https://github.com/python/mypy

使ってみる

インストール

uvを利用して以下のようにインストールします。

uv new mypy_test -p 3.12 && mypy_test
uv add mypy

https://zenn.dev/akasan/articles/39f81f8bd15790

簡単なサンプルコードを作ってみる

まず最初に、以下のようなサンプルコードを作ってみます。

simple_function.py
def add(x: int, y: int) -> int:
    return x + y


x = 10
y = 20
z = add(x, y)
print(f"{z=}")

add関数はxとy二つのint型データを受け取り、その足し算をint型で返すというものを用意しました。まずはx、y両方にint型を設定した場合、つまり望んだデータ型を受け取る場合のチェックをしてみます。
mypyを適用するには以下のようにすることで対応できます。その結果、問題はなくSuccessと表示されました。

uv run mypy simple_function.py

# 結果
Success: no issues found in 1 source file

次は試しにxの値をx = 10.5のようにしてfloat型にしたときにどのような結果になるかみてみました。その結果、add関数の呼び出しでxは本来int型となるはずだがfloat型を受け取ったと検知できています。

simple_func.py:7: error: Argument 1 to "add" has incompatible type "float"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

それではadd関数の戻り値をreturn float(x + y)のようにしたらどのような結果になるかみてみます。実行してみると、戻り値はint型を期待しているのにfloat型になっていることが検知できました。

simple_func.py:2: error: Incompatible return value type (got "float", expected "int")  [return-value]
Found 1 error in 1 file (checked 1 source file)

typingの使い方

Pythonには組み込みのtypingモジュールがあり、アノテーションをする場合はうまく使うことで可読性が上がります。

例えばリストの値の合計を計算したいとします。愚直に実装すれば以下のようになります。

sum_sequence.py
def _sum(x: list[int]) -> int:
    return sum(x)


x = [i for i in range(10)]
sum_result = _sum(x)

もちろんこれはアノテーションが正しいのでエラーが発生することはありません。

uv run mypy sum_sequence.py

# 結果
Success: no issues found in 1 source file

ここで、例えばlistではなくtupleを使うことに変更した場合エラーになります(tupleの方がlistと比較してメモリ使用量などが小さくなります)。

sum_sequence.py
def _sum(x: list[int]) -> int:
    return sum(x)


x = tuple([i for i in range(10)])
sum_result = _sum(x)
uv run mypy sum_sequence.py

# 結果
sum_sequence.py:6: error: Argument 1 to "_sum" has incompatible type "tuple[int, ...]"; expected "list[int]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

listを使うことで確定している場合は元の実装が良いですが、シーケンスが指定されることを想定する場合、例えば以下のようにするとtupleでもlistでも受け入れることができるようになります。

sum_sequence.py
from typing import Sequence

def _sum(x: Sequence[int]) -> int:
    return sum(x)


x = tuple([i for i in range(10)])
sum_result = _sum(x)
uv run mypy sum_sequence.py

# 結果
Success: no issues found in 1 source file

Anyの利用について

PythonでOSSなどをみたりすると、しばしばtyping.Anyが利用されているのを目にします。Anyを指定するとどのような値が帰ってくる可能性があることを明示できますが、正直あまり利用しない方がいいと思っています。なぜかというと、Anyというアノテーションはアノテーションをしているようで一切の情報を与えることができないからです。

そのため、mypyでAnyが使われていれば検知させたいですが、デフォルトでは検知されません。mypyでAnyの利用を検知させるには--disallow-any-exprオプションを指定します。例えば先ほどの例でSequenceをAnyにしてみます。

sum_sequence_any.py
from typing import Any

def _sum(x: Any) -> int:
    return sum(x)


x = tuple([i for i in range(10)])
sum_result = _sum(x)

オプションなしで実行すると以下のようにエラーは検知されません。

uv run mypy sum_sequence_any.py

# 結果
Success: no issues found in 1 source file

それでは--disallow-any-exprをつけて実行すると以下のように検知されます。

uv run mypy sum_sequence_any.py --disallow-any-expr

# 結果
sum_sequence_any.py:4: error: Expression has type "Any"  [misc]
Found 1 error in 1 file (checked 1 source file)

まとめ

今回はmypyを使ったデモを簡単にお見せしました。Pythonは動的型付け言語でありデータ型に関する厳密なチェックが実施されにくい傾向にあるかと思います。Pythonを利用される方はぜひmypyなどの型チェッカーをぜひ利用してみてはいかがでしょうか。

Discussion