はじめに
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"))
SampleId
と Sample2Id
は別物ですが、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 の導入
最初の一歩
uv add mypy --dev
mypy your_module.py
前述のバグ例は 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 補完の精度が上がり、安心して速く書ける
- 単体テストは本来の目的(ロジックや仕様の確認)に専念できる
感じた課題点
-
外部ライブラリの型不足
- 多くのサードパーティライブラリに型定義がなく、mypy がチェックできない部分が多い。
-
ignore_missing_imports
を多用せざるを得ず、「せっかくの静的解析なのに…」という気持ちになりやすい。
-
動的コードに弱い
- Python らしい「柔軟さ」がそのまま弱点になる。
- 例: リフレクション、動的属性追加、メタクラスなどは解析できず、常にエラー扱い。
-
# type: ignore
が増えやすい- 型定義の欠落や推論の限界により、どうしても抑えきれないエラーが出る。
- 放置すると「本当に必要な無視」と「雑に消した無視」が混在して管理が難しくなる。
まとめ
課題 | 望ましいタイミング | 採用した手段 |
---|---|---|
引数取り違え | 書いた瞬間 | mypy(静的解析)+キーワード専用引数 |
値の差し替え事故 | 設計時 | 不変データクラス |
str と int の混在 |
書いた瞬間 | mypy/NewType |
値域・相関の検証 | 実行時 | pydantic / beartype など |
Python は動的型付けで柔軟な一方、「実行しないと気づけない」リスクがあります。
mypy を核に設計や実行時検証を組み合わせることで、静的型付き言語に近い「書いた瞬間に気づける」開発体験へ寄せることができました。