mypy strict モードで型安全なPythonを書く
概要
Python の型ヒント(type hints)は書けるだけでは意味がなく、静的解析ツールで検証してはじめて価値が出ます。その検証を担うのが mypy です。
ところが mypy をデフォルト設定で回しても、型注釈のない関数はまるごと素通しされます。「導入したのにバグを取りこぼす」状態になりやすい。この記事では --strict モードを起点に、実際に型エラーを検出できるプロジェクト構成をハンズオンで作っていきます。
環境
- Python 3.11
- mypy 1.x
python -m venv .venv source .venv/bin/activate pip install mypy mypy --version
デフォルト mypy が見逃すもの
まず、なぜ strict が必要かを体感します。
def add(a, b):
return a + b
result: int = add("1", "2") # 本当は文字列連結になる
print(result * 10)
add に型注釈がありません。デフォルトの mypy はこう言います。
$ mypy sample.py Success: no issues found in 1 source file
add("1", "2") は "12" を返し、result: int に代入されているのにエラーになりません。注釈がない関数の中身と戻り値は「型不明(Any)」として扱われ、Any は何にでも代入できるからです。これが「mypy 入れたのにバグが出る」の典型パターンです。
strict モードを有効にする
$ mypy --strict sample.py sample.py:1: error: Function is missing a type annotation [no-untyped-def] sample.py:4: error: Argument 1 to "add" has incompatible type "str"; expected "int" [arg-type] Found 2 errors in 1 source file
一気に検出されました。--strict は複数の厳格チェックをまとめて有効にするショートカットです。主なものは次のとおりです。
| フラグ | 内容 |
|---|---|
disallow_untyped_defs |
型注釈のない関数定義を禁止 |
disallow_any_generics |
裸のジェネリクスを禁止(list[int] を要求) |
warn_return_any |
Any を返す関数に警告 |
no_implicit_optional |
暗黙の Optional を禁止 |
warn_unused_ignores |
不要な # type: ignore に警告 |
check_untyped_defs |
注釈のない関数の中身も型チェック |
コードを直します。
def add(a: int, b: int) -> int:
return a + b
result: int = add(1, 2)
print(result * 10)
設定ファイルに固定する
CLI フラグは忘れます。pyproject.toml に書いてリポジトリで共有しましょう。
[tool.mypy] python_version = "3.11" strict = true warn_unreachable = true warn_redundant_casts = true
以降は mypy . だけで strict が効きます。
実践1: Optional を明示的に扱う
def find_user(user_id: int) -> str | None:
users = {1: "alice", 2: "bob"}
return users.get(user_id)
name = find_user(3)
print(name.upper()) # None かもしれないのに .upper() を呼んでいる
$ mypy --strict sample.py error: Item "None" of "str | None" has no attribute "upper" [union-attr]
None チェックを強制されます。
name = find_user(3)
if name is not None:
print(name.upper())
mypy は if ブロック内で name を str に絞り込み(narrowing)してくれます。
実践2: 段階的に strict を導入する
既存プロジェクトにいきなり strict = true を入れると、数百件のエラーが出ます。現実的にはモジュール単位で段階導入します。
[tool.mypy] python_version = "3.11" ignore_missing_imports = true [[tool.mypy.overrides]] module = ["myapp.domain.*", "myapp.services.*"] disallow_untyped_defs = true disallow_any_generics = true warn_return_any = true
「新しく触るコードから型を固める」方針にすれば、レガシーを止めずに型安全な領域を広げられます。
実践3: サードパーティの型がないとき
pip install types-requests # requests 用の公式スタブ
スタブが存在しないライブラリは、そのモジュールだけ緩めます。
[[tool.mypy.overrides]] module = ["some_untyped_lib.*"] ignore_missing_imports = true
確認方法
CI に組み込んで、型エラーでビルドを落とします。
- name: Type check
run: |
pip install mypy
mypy .
pre-commit に載せると、コミット時点で弾けます。
repos:
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
hooks:
- id: mypy
args: [--strict]
注意点
Anyは伝播する。1か所で許すと周囲の型チェックが無効化されます。warn_return_anyで入口を塞ぐのが有効です。# type: ignoreにはエラーコードを付ける(# type: ignore[arg-type])。別種のエラー混入を検出できます。strict = trueはバージョンで内容が増える。mypy 更新時はまとめて対応する前提で。- 実行時の型チェックではない。外部入力の実行時検証は pydantic 等が別途必要です。
まとめ
- デフォルト mypy は注釈のない関数を素通しするため、バグを取りこぼす。
strict = trueで「注釈必須・Any 警告・暗黙 Optional 禁止」がまとめて効く。- 既存プロジェクトは
overridesでモジュール単位に段階導入するのが現実解。 - CI と pre-commit に載せて初めて「守られる型」になる。
型ヒントは書いた時点では単なるコメントです。strict モードの mypy で検証してはじめて、リファクタリングを支える資産になります。

