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 ブロック内で namestr に絞り込み(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 で検証してはじめて、リファクタリングを支える資産になります。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください