4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonの型チェックでmypy使ってみた

Posted at

はじめに

Python を使った開発で「実行するまで(あるいは単体テストを回すまで)間違いに気づけない」と悩んだことはありませんか。
もちろん単体テストを書けば検出できますが、もっと早い段階――書いた瞬間に IDE 上で警告される体験を求めていました。
これは静的型付き言語なら当たり前に得られるフィードバックです。

そこで、いくつかの対策を検討した結果、静的型チェッカーである mypy を導入しました。この記事では、遭遇したバグ例、対策の比較、mypy 導入の効果と限界について整理します。

結論:mypyで「書いた時点」で気づけるようになり、開発が楽になった

  • mypy は実行やテストの前に「引数の取り違え」や「演算子の型不一致」を警告してくれます。
  • 設計上の工夫(キーワード専用引数や不変データクラス)や、必要に応じた実行時検証と組み合わせることで強力になります。
  • ただし、外部ライブラリの型不足や動的コード、値の範囲検証などはカバーできないため、補完策も必要です。

きっかけ:テストで発覚するが“遅い”バグ

例1: 同じ形のクラスを取り違え

@dataclass(frozen=True, slots=True)
class SampleId: 
    value: str

@dataclass(frozen=True, slots=True)
class Sample2Id: 
    value: str

class SampleClass:
    def __init__(self, sample_id: SampleId, sample2_id: Sample2Id) -> None:
        self.sample_id = sample_id
        self.sample2_id = sample2_id

# Python は動的型付けのためそのまま実行できる
sample = SampleClass(Sample2Id("67890"), SampleId("12345"))

SampleIdSample2Id は別物ですが、Python は動的型付けのためそのまま実行できてしまいます。

例2: 数値ラッパと文字列ラッパの取り違え

@dataclass(frozen=True, slots=True)
class SampleNumber: 
    value: int

@dataclass(frozen=True, slots=True)
class SampleString: 
    value: str

class SampleClass2:
    def __init__(self, sample_number: SampleNumber) -> None:
        self.sample_number = sample_number

    def display_info(self) -> None:
        print(self.sample_number.value + 5)

# テスト実行で初めて TypeError になる
sample2 = SampleClass2(sample_number=SampleString("10"))
sample2.display_info()

単体テストで検出はできますが、そこに至るまでの時間が無駄になります。
そこで「書いた瞬間に気づきたい」と強く思うようになりました。

対策の検討

設計の工夫

  • コンストラクタをキーワード専用引数にして、位置引数での取り違えを禁止
  • NewType を使って軽量に別型化(例: SampleId = NewType("SampleId", str))

実行時検証

  • pydantic / attrs:宣言的なバリデーション
  • beartype / typeguard:型ヒントを実行時チェックに昇格

静的解析(理想)

  • mypy / Pyright / Pyre:書いた時点で IDE 上に赤線を出す

最終的に「mypy + 設計の工夫」を採用し、部分的に実行時検証を併用することにしました。

mypy の導入

mypy 公式ドキュメント

最初の一歩

uv add mypy --dev
mypy your_module.py

前述のバグ例は mypy を通すと以下のようなエラーが出ます:

例1
mypy_check1

例2
mypy_check2

実行やテストをする前に「型が違う」ことが即座に検出されます。

段階的な設定例

mypy 設定ファイルドキュメント

[mypy]
python_version = 3.12
disallow_untyped_defs = True
no_implicit_optional = True
warn_return_any = True
warn_unused_ignores = True

[mypy-some_third_party.*]
ignore_missing_imports = True   # 広域ではなく必要箇所だけ
  • まずは disallow_untyped_defs などから始め、徐々に厳格化
  • 外部ライブラリはピンポイントで ignore_missing_imports
  • 余力があればモジュール単位で --strict に寄せる

VS Codeとの統合

VS Code には Mypy Type Checker という拡張機能がありますが、現時点ではプレビュー版です。
実運用では挙動の安定性に注意し、必要に応じて mypy --watch を並行実行するなどの工夫が求められます。

導入後の変化

  • テストに到達する前に粗いミスを潰せるため、開発サイクルを短縮できる
  • コードレビューは「設計意図」に集中でき、表層的な取り違えは事前に排除可能
  • IDE 補完の精度が上がり、安心して速く書ける
  • 単体テストは本来の目的(ロジックや仕様の確認)に専念できる

感じた課題点

  1. 外部ライブラリの型不足

    • 多くのサードパーティライブラリに型定義がなく、mypy がチェックできない部分が多い。
    • ignore_missing_imports を多用せざるを得ず、「せっかくの静的解析なのに…」という気持ちになりやすい。
  2. 動的コードに弱い

    • Python らしい「柔軟さ」がそのまま弱点になる。
    • 例: リフレクション、動的属性追加、メタクラスなどは解析できず、常にエラー扱い。
  3. # type: ignore が増えやすい

    • 型定義の欠落や推論の限界により、どうしても抑えきれないエラーが出る。
    • 放置すると「本当に必要な無視」と「雑に消した無視」が混在して管理が難しくなる。

まとめ

課題 望ましいタイミング 採用した手段
引数取り違え 書いた瞬間 mypy(静的解析)+キーワード専用引数
値の差し替え事故 設計時 不変データクラス
strint の混在 書いた瞬間 mypy/NewType
値域・相関の検証 実行時 pydantic / beartype など

Python は動的型付けで柔軟な一方、「実行しないと気づけない」リスクがあります。
mypy を核に設計や実行時検証を組み合わせることで、静的型付き言語に近い「書いた瞬間に気づける」開発体験へ寄せることができました。

参考リンク

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?