Python | テスト・設計・品質:mypy

Python Python
スポンサーリンク

mypyって何?一言でいうと「Pythonコードに“型のテスト”をかけるツール」

mypy は、Python コードに書いた「型ヒント(type hints)」を読み取って、
その使い方が矛盾していないかをチェックしてくれる 静的型チェッカー です。

ポイントはここです。

Python 自体は動的型付けのまま(実行時には型チェックしない)
mypy は「コードを実行せずに」型の整合性だけをチェックする
テストを書く前に、「型レベルのバグ」をかなり潰せる

つまり、mypy は「型に関するテスト」を自動でやってくれるツールだと思ってください。


まずは「型ヒントなし」と「mypy前提」の違いを体感する

型ヒントなしの世界:バグは実行してから分かる

例えば、こんなコードがあるとします。

def add(a, b):
    return a + b

result = add("1", "2")
print(result)
Python

Python 的には、これは普通に動きます。
"1" + "2""12" になるので、エラーにはなりません。

でも、もし「数値として足し算したい」つもりだったなら、
これはバグです。

型ヒントも mypy もない世界では、
こういう「意図と違う使い方」は、
実行してみるまで分かりません。

型ヒント+mypyの世界:実行前に「おかしいよ」と言ってくれる

同じコードに型ヒントを付けてみます。

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

result = add("1", "2")
print(result)
Python

ここで mypy を走らせると、こんな感じのエラーが出ます(イメージ):

example.py:5:15: error: Argument 1 to "add" has incompatible type "str"; expected "int"
example.py:5:20: error: Argument 2 to "add" has incompatible type "str"; expected "int"

addint を受け取るはずなのに、str を渡しているよ」と教えてくれるわけです。

これが mypy の本質です。

「型ヒントで“こう使うつもり”を書いておく」
→ 「mypy が“その通りに使われているか”をチェックする」

このセットで初めて、型ヒントが本気で効いてきます。


mypyが得意なことを具体例で見る

1. 間違った型の引数・戻り値を検出する

もう少し現実的な例を見てみます。

def format_price(price: int, currency: str) -> str:
    return f"{price:,} {currency}"

def main() -> None:
    text = format_price("1000", "JPY")
    print(text)
Python

mypy をかけると、

error: Argument 1 to "format_price" has incompatible type "str"; expected "int"

と怒られます。

price は int のつもりなのに、str を渡している」
という矛盾を、実行前に見つけてくれます。

2. Optional(Noneかもしれない)を雑に扱うと怒ってくれる

Optional を使った典型的な例です。

from typing import Optional

def find_user_name(user_id: int) -> Optional[str]:
    if user_id == 1:
        return "Taro"
    return None

def main() -> None:
    name = find_user_name(1)
    print(name.upper())
Python

find_user_namestr | None を返す可能性があります。
mypy はここを見逃しません。

error: Item "None" of "Optional[str]" has no attribute "upper"

name は None かもしれないのに、upper を呼んでいる」と教えてくれます。

これを受けて、コードをこう直します。

def main() -> None:
    name = find_user_name(1)
    if name is not None:
        print(name.upper())
    else:
        print("ユーザーが見つかりませんでした")
Python

mypy は、「None の可能性をちゃんと潰したか?」を
型レベルでチェックしてくれるわけです。

これは、実行時の AttributeErrorTypeError
かなり減らしてくれます。

3. 辞書やリストの中身の型もチェックしてくれる

from typing import List

def total(values: List[int]) -> int:
    return sum(values)

nums: List[int] = [1, 2, 3]
nums.append("4")  # うっかり文字列を入れてしまった

print(total(nums))
Python

mypy はここでも怒ります。

error: Argument 1 to "append" of "list" has incompatible type "str"; expected "int"

List[int]str を入れようとしている」と教えてくれます。

リストや辞書の「中身の型」まで見てくれるのが、
mypy の強いところです。


mypyとテストの関係(ここが一番大事な話)

mypyは「テストの代わり」ではないが、「テスト前のフィルター」になる

mypy は、ロジックの正しさまでは見てくれません。

例えば、こういうバグは mypy では分かりません。

def add(a: int, b: int) -> int:
    return a - b  # 間違って引き算している
Python

型的には「int を受け取って int を返している」ので、
mypy 的には OK です。

だから、ユニットテストは絶対に必要です。

ただし、mypy を入れておくと、

引数・戻り値の型の取り違え
None の扱いミス
リスト・辞書の中身の型ミス
Protocol / TypedDict / Literal などの使い方の矛盾

といった「型レベルのバグ」を、
テストを書く前にかなり潰せます。

結果として、

テストは「ロジックの正しさ」に集中できる
実行してすぐ落ちるような初歩的なバグが減る

という効果が出ます。

「mypyが通る=型の約束を守れている」という安心感

型ヒントをちゃんと書いて、
mypy が全部通る状態になっていると、

「少なくとも、型の約束は守れている」

という安心感が得られます。

これは、リファクタリングするときに特に効きます。

関数の引数を変えた
戻り値の型を変えた
クラスのメソッドのシグネチャを変えた

こういう変更をしたとき、
mypy が「ここも直さないとダメだよ」と教えてくれるので、
「直し漏れ」をかなり防げます。


mypyを導入するときにハマりやすいポイントとコツ

いきなり「全ファイルを厳格チェック」はだいたい挫折する

既存プロジェクトに mypy を入れて、
いきなり全ファイルを厳しくチェックすると、
エラーだらけになって心が折れます。

現実的なステップはこんな感じです。

新しく書くコードには、最初から型ヒントを書く
mypy を「緩めの設定」で導入する(ignore_missing_imports など)
既存コードは、触るところから少しずつ型を付けていく

「新しいコードは型付き・mypy 通過が前提」
「古いコードは、触るたびに少しずつ型を付ける」

くらいのペースがちょうどいいです。

最初は「関数の引数と戻り値」だけでも十分価値がある

mypy を活かすには、
全部の変数に型ヒントを付ける必要はありません。

まずは、

外から呼ばれる関数の引数
その戻り値

ここにだけでも型ヒントを付けて、
mypy を回してみるといいです。

それだけでも、

間違った型の引数を渡していないか
戻り値の使い方が矛盾していないか

をかなりチェックしてくれます。

「関数の境界」に型を付けるだけで、
設計の輪郭がはっきりしてきます。


mypyと他のツール(ruff, black, pytest)との役割分担

ざっくり役割を整理するとこうなる

black
コードの見た目(インデント・改行・空白)を自動整形する

ruff
スタイル違反・未使用変数・簡単なバグの匂いを検出する(+一部自動修正)

mypy
型ヒントに基づいて、「型の約束」が守られているかをチェックする

pytest(などのテストフレームワーク)
ロジックが仕様通りに動いているかを実行して確かめる

この4つが揃うと、

見た目はきれい(black)
雑なミスは減る(ruff)
型レベルの矛盾は減る(mypy)
ロジックのバグはテストで守る(pytest)

という、かなり強い布陣になります。


初心者がmypyとどう付き合うといいか

「型ヒントを書いたら、必ずmypyを一回回す」を習慣にする

最初は、
小さなファイルでいいので、

型ヒントを書く
mypy を回す
怒られたところを直す

というサイクルを何度か回してみてください。

そのうち、

この書き方だと mypy に怒られるな
ここは Optional にした方がいいな
ここは Union じゃなくて関数を分けた方がいいな

という感覚が、体で分かってきます。

「mypyに怒られる=ダメ」ではなく「設計のヒント」として受け取る

mypy のエラーは、
単なる「赤いバツ」ではなく、
「設計を見直すヒント」です。

Optional の扱いが雑になっていないか
辞書やリストの中身の型が曖昧になっていないか
関数の責務が広すぎて、型が複雑になっていないか

mypy に怒られたときは、
「どうやって黙らせるか」ではなく、
「どう設計を整理すると自然に通るか」を考えると、
一気にレベルが上がります。


まとめ(mypyは「型ヒントを本物の武器に変えるテスター」)

mypy を初心者目線で整理すると、こうなります。

mypy は、Python コードに書いた型ヒントを読み取って、「その型の約束通りに使われているか」を実行前にチェックする静的型チェッカー。
引数・戻り値・Optional・リストや辞書の中身などの型の矛盾を早期に見つけてくれるので、「テストを書く前に潰せるバグ」がかなり増え、リファクタリングの安心感も大きくなる。
全部を一気に厳しくするのではなく、「新しいコードの関数の引数・戻り値から」「触るところから少しずつ」というペースで導入するのが現実的で、挫折しにくい。
black・ruff・pytest などと組み合わせると、「見た目」「スタイル」「型」「ロジック」のそれぞれを別々のツールで守れるようになり、コードの品質が一段上のレベルで安定する。

タイトルとURLをコピーしました