閉じる

型ヒント×mypyでバグを未然に防ぐ: 失敗しない設定と使い方

型ヒントはコードの「意図」を明文化し、mypyはその意図と実装のズレを実行前に指摘します

本記事ではPython初心者の方でもつまずかないよう、基礎から導入、設定、運用のコツまでを段階的に解説します。

最後まで読めば、日々のバグがグッと減らせる実践的な型チェック環境を構築できます。

型ヒント×mypyの基礎(Python初心者向け)

型ヒントとは何か

型ヒントは、変数や関数の引数・戻り値に「この型を想定しています」と注釈する仕組みです。

Pythonは動的型付け言語ですが、型ヒントはドキュメント兼コンパイラのような役割を担う静的解析ツールに情報を提供します。

実行速度はほぼ変わりませんが、IDEの補完精度や読みやすさ、バグ検出力が向上します。

ランタイムでは強制されない例

Pythonは型ヒントを守らなくても実行されます。

だからこそ、静的解析ツールの出番です。

Python
# ファイル名: demo_runtime_no_enforce.py
def number_to_str(n: int) -> int:  # 戻り値をintと書いているが…
    # 実際には文字列を返している
    return str(n)

result = number_to_str(3)
print(result, type(result))  # 実行は成功してしまう
実行結果
3 <class 'str'>

このズレを実行前に検出するのがmypyです。

Shell
# mypyの実行例
mypy demo_runtime_no_enforce.py
実行結果
demo_runtime_no_enforce.py:2: error: Incompatible return value type (got "str", expected "int")  [return-value]
Found 1 error in 1 file (checked 1 source file)

実際にmypyを利用するには別途導入が必要です。

mypyとは何か

mypyはPython用の静的型チェッカーです。

型ヒント(PEP 484)を読み取り、「この関数はintを返すはずなのにstrを返している」といった矛盾を指摘します。

エディタやCIと連携することで、間違いがコードレビュー前に見つかるようになります。

静的解析で分かること

静的解析は実行せずにソースコードだけを見て検査します。

例えば次のような問題が分かります。

  • 関数の引数・戻り値の型不一致
  • None安全性(Optionalの未チェック使用)
  • 辞書やリストの要素型ミスマッチ(ジェネリクス)
  • 未到達コードや未返却パスの検出
  • サードパーティーAPIの誤用(型スタブがある場合)

バグ予防に効く理由

型は「期待」と「契約」をコード化する仕組みです。

型に違反する変更が入るとmypyが即座に赤信号を出すため、バグが本番に到達する前に止められます。

また、型があるとリファクタリング時に影響範囲が明確になり、安全に変更できます。

実行時エラーとの違い

静的解析と実行時(例外処理)は補完し合う関係です。

どちらも大切です。

表: 静的解析と実行時エラーの比較

観点静的解析(mypy)実行時(例外/try-except)
実行の有無不要必要
発見タイミングコードを書く段階実際にその行が走ったとき
検出が得意型不整合、None取り扱いミス入出力エラー、ネットワーク障害、ファイル不存在
役割仕様とのズレを未然に防止現実世界の失敗を安全に処理

mypyは仕様ズレの未然防止、try-exceptは現場の失敗からの復旧、と覚えると整理しやすいです。

mypyの導入と基本の使い方

インストールと初回チェック

まずはプロジェクトにmypyを入れます。

仮想環境を使うのが安全です。

Shell
# 仮想環境の作成と有効化(例: venv)
python -m venv .venv
# Windowsなら .venv\Scripts\activate、macOS/Linuxなら:
source .venv/bin/activate

# mypyのインストール
pip install mypy

# バージョン確認
mypy --version

# プロジェクト直下をまとめてチェック
mypy .
実行結果
mypy 1.x.x (compiled: yes)
Success: no issues found in X source files

初回で大量のエラーが出る場合も心配ありません。

後述の「段階導入」「設定」で少しずつ整えていきます。

最小の型ヒント(変数/引数/戻り値)

Python 3.9+なら組み込みのジェネリクス表記(例: list[int])が使えます。

Python
# ファイル名: basics_minimal_hints.py
from typing import Optional

# 変数の型注釈
count: int = 0
title: Optional[str] = None  # Noneを取り得る

# 引数と戻り値の注釈
def greet(name: str) -> str:
    return f"Hello, {name}!"

# リストや辞書などのコレクション
def sum_all(xs: list[int]) -> int:
    s = 0
    for x in xs:
        s += x
    return s

print(greet("Alice"))
print(sum_all([1, 2, 3]))
実行結果
Hello, Alice!
6

よくある型エラー例と直し方

初心者がつまずきやすいパターンを3つ取り上げます。

1) Optionalを未チェックで使う

Python
# ファイル名: error_optional.py
from typing import Optional

def upper_or_default(s: Optional[str]) -> str:
    # mypyはsがNoneの可能性を指摘する
    return s.upper()  # エラー: "None"にはupperがない

mypyの出力例:

text
error_optional.py:5: error: Item "None" of "Optional[str]" has no attribute "upper"  [union-attr]

修正例(ガードを入れる):

Python
# ファイル名: fix_optional.py
from typing import Optional

def upper_or_default(s: Optional[str]) -> str:
    if s is None:
        return ""
    return s.upper()

2) リストの要素型ミスマッチ

Python
# ファイル名: error_list_type.py
names: list[str] = ["Alice", "Bob"]
names.append(123)  # エラー: intはlist[str]に入れられない

修正例(型を統一する/入力を変換する):

Python
# ファイル名: fix_list_type.py
names: list[str] = ["Alice", "Bob"]
names.append(str(123))  # 文字列に変換してから追加

3) 戻り値の取りうるパスが未カバー

Python
# ファイル名: error_missing_return.py
def sign(n: int) -> str:
    if n > 0:
        return "plus"
    elif n < 0:
        return "minus"
    # n == 0 の場合に戻り値がない → mypyが検出

修正例:

Python
# ファイル名: fix_missing_return.py
def sign(n: int) -> str:
    if n > 0:
        return "plus"
    elif n < 0:
        return "minus"
    else:
        return "zero"

型エラーを無視しないコツ

基本方針は「ignoreを安易に使わない」です。

どうしても必要なら、範囲を最小化し、理由をコメントで残します。

Python
# ファイル名: cautious_ignore.py
from typing import Any, cast

def parse_user_id(raw: Any) -> int:
    # 外部からの入力など、Anyが避けられない箇所
    if isinstance(raw, str) and raw.isdigit():
        return int(raw)
    # どうしても型が不明確ならcastを使って意図を明示
    # mypyエラーを抑制しつつ、後で置き換えやすい
    return cast(int, raw)  # type: ignore[return-value]  # API制約上ここは後で直す

設定ではwarn-unused-ignores = Trueを有効にし、無意味なignoreの野放しを防ぎます

またreveal_type(expr)を使うとmypyが推論した型を表示でき、デバッグに役立ちます。

Python
# ファイル名: reveal_type_demo.py
from typing import Optional

def f(s: Optional[str]) -> None:
    reveal_type(s)  # mypy専用: "Optional[str]" と表示される
Shell
mypy reveal_type_demo.py
実行結果
reveal_type_demo.py:5: note: Revealed type is "builtins.str | None"
Success: no issues found in 1 source file

失敗しないmypy設定(初心者向け)

設定ファイルの置き場所(mypy.ini/pyproject.toml)

プロジェクト直下にpyproject.tomlmypy.iniを置きます。

最近はpyproject.tomlが主流です。

TOML
# ファイル名: pyproject.toml (推奨)
[tool.mypy]
python_version = "3.12"
pretty = true
show_error_codes = true
warn_unused_ignores = true
warn_redundant_casts = true
no_implicit_optional = true
check_untyped_defs = true
INI
# ファイル名: mypy.ini (代替)
[mypy]
python_version = 3.12
pretty = True
show_error_codes = True
warn_unused_ignores = True
warn_redundant_casts = True
no_implicit_optional = True
check_untyped_defs = True

モノレポなどではサブパッケージ単位でセクションを分けられます。

TOML
[tool.mypy.my_app.*]
disallow_untyped_defs = true

はじめての安全設定(推奨オプション)

最初から--strictはハードルが高いので、実務で安全かつ導入しやすい最小セットを推奨します。

表: 初心者向けの実用オプション

オプション役割効果
show_error_codesエラーコード表示後でピンポイントに無視・検索がしやすい
warn_unused_ignores使われないignoreを警告無意味なignoreの蓄積を防止
warn_redundant_casts不要なcastを警告cast乱用の抑制
no_implicit_optionalTとOptional[T]の曖昧さを排除None安全性を高める
check_untyped_defs注釈なし関数も内部をチェック漸進導入に有効

加えて、プロジェクトのPythonバージョンをpython_versionで明示しましょう。

少しずつ厳しくする方法(–strictへの道)

段階的強化で挫折を防ぎます。

以下を順に上げていくとスムーズです。

TOML
# ステップ1: 基本の安全ライン
[tool.mypy]
warn_unused_ignores = true
warn_redundant_casts = true
no_implicit_optional = true
check_untyped_defs = true

# ステップ2: 返り値・Anyの可視化を強める
warn_return_any = true
disallow_any_generics = true

# ステップ3: 未注釈の流入を止める
disallow_untyped_defs = true
disallow_incomplete_defs = true

# ステップ4: さらに厳格化(ほぼ--strict相当へ)
warn_unused_configs = true
no_implicit_reexport = true
strict_equality = true

最後にmypy --strictへ到達できれば理想ですが、プロジェクトの状況に合わせて妥協点を定めるのも現実的です。

サードパーティーの型(stub/typing-extensions)

外部ライブラリの多くは型スタブを持ちます。

なければtypes-*パッケージを探します。

Shell
# 例: requests の型スタブ
pip install requests types-requests
Python
# ファイル名: use_requests_typed.py
import requests

def fetch_json(url: str) -> dict:
    r = requests.get(url, timeout=10)
    r.raise_for_status()
    return r.json()  # 型スタブがあると戻り値がdict扱いになり、後続の型検査が効く

古いPythonで新しい型機能を使いたい場合はtyping_extensionsを利用します。

TypedDictProtocolなどが補えます。

Shell
pip install typing-extensions
Python
# ファイル名: typing_extensions_demo.py
from typing_extensions import TypedDict, Protocol

class User(TypedDict):
    id: int
    name: str

class Greeter(Protocol):
    def greet(self, name: str) -> str: ...

運用のコツとベストプラクティス

既存コードへの段階導入

既存コードに一気に適用するとエラーの洪水になります。

新規/変更箇所から注釈を付ける、フォルダ単位で対象を広げる、を繰り返しましょう。

excludeで一時的に対象外にするのも有効です。

TOML
# pyproject.toml
[tool.mypy]
exclude = [
  "legacy/.*",
]

ignore-missing-importsの常用は非推奨です。

やむを得ず使う場合は、モジュール単位で限定的に適用し、のちに型スタブ導入で外す計画を立てます。

TOML
# 特定モジュールだけ
[[tool.mypy.overrides]]
module = "some_legacy_lib.*"
ignore_missing_imports = true

テストやCIでの自動チェック

テストと同列に型チェックをCIへ組み込みます。

失敗したらマージできない仕組みにするのが効果的です。

YAML
# ファイル名: .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  type-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python -m pip install -U pip
      - run: pip install -r requirements.txt
      - run: pip install mypy
      - run: mypy .

開発体験を上げるにはpre-commitフックも便利です。

YAML
# ファイル名: .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.10.0
    hooks:
      - id: mypy
        additional_dependencies:
          - types-requests

エディタ(VSCode)連携で早めに気づく

VSCodeではPylanceの型チェックとmypyを併用できます。

Pylanceは即時フィードバック、mypyはプロジェクト全体の厳密検査という役割分担が現実的です。

JSON
// ファイル名: .vscode/settings.json
{
  "python.analysis.typeCheckingMode": "basic",  // 余力があれば "strict"
  "mypy-type-checker.args": [
    "--pretty",
    "--show-error-codes"
  ],
  "mypy-type-checker.severity": "error",       // 問題パネルにエラー表示
  "mypy-type-checker.preferDaemon": true       // 高速化(dmypy)
}

VSCode拡張「Mypy Type Checker」を入れると保存時に自動チェックされ、赤線でその場に間違いが出ます

つまずきやすい型(Optional/Any/None)の扱い

Optional(T | None)

OptionalはNoneを含む型です。

使う前に必ずガードしましょう。

Python
# ファイル名: optional_patterns.py
from typing import Optional

def length_or_zero(s: Optional[str]) -> int:
    if s is None:
        return 0
    return len(s)

def require_non_null(s: Optional[str]) -> int:
    assert s is not None  # 実行時にも守られる前提条件
    return len(s)

Any

Anyは「何でも通す」特別扱いで、バグがすり抜けやすいです。

境界(外部I/O)に閉じ込め、内部へ入れる前にパースして型を確定しましょう。

Python
# ファイル名: any_boundary.py
from typing import Any, TypedDict, cast

class User(TypedDict):
    id: int
    name: str

def parse_user(payload: Any) -> User:
    # ざっくり検証してから型付け
    if not isinstance(payload, dict):
        raise ValueError("Invalid payload")
    if not isinstance(payload.get("id"), int) or not isinstance(payload.get("name"), str):
        raise ValueError("Invalid fields")
    return cast(User, payload)

Noneの初期化と再代入

変数を後から値で埋める場合はユニオン型を明示します。

Python
# ファイル名: none_init.py
token: str | None = None
# 後でセット
token = "abc123"

このパターンならmypyは「最初はNone、以降はstr」という意図を理解してくれます。

まとめ

型ヒント×mypyは、バグを「発生後に追う」から「発生前に止める」へ発想を転換させる実用的な武器です。

基礎として型ヒントの意味を押さえ、mypyで静的解析を回し、設定を段階的に厳しくしながら、CI・エディタ統合で常時チェックを自動化すると効果が最大化します。

OptionalやAny、Noneといった落とし穴も、ガード・パース・境界の設計で回避できます。

まずは小さく導入し、成功体験を積みながら適用範囲を広げていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!