型ヒントはコードの「意図」を明文化し、mypyはその意図と実装のズレを実行前に指摘します。
本記事ではPython初心者の方でもつまずかないよう、基礎から導入、設定、運用のコツまでを段階的に解説します。
最後まで読めば、日々のバグがグッと減らせる実践的な型チェック環境を構築できます。
型ヒント×mypyの基礎(Python初心者向け)
型ヒントとは何か
型ヒントは、変数や関数の引数・戻り値に「この型を想定しています」と注釈する仕組みです。
Pythonは動的型付け言語ですが、型ヒントはドキュメント兼コンパイラのような役割を担う静的解析ツールに情報を提供します。
実行速度はほぼ変わりませんが、IDEの補完精度や読みやすさ、バグ検出力が向上します。
ランタイムでは強制されない例
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です。
# 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を入れます。
仮想環境を使うのが安全です。
# 仮想環境の作成と有効化(例: 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]
)が使えます。
# ファイル名: 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を未チェックで使う
# ファイル名: error_optional.py
from typing import Optional
def upper_or_default(s: Optional[str]) -> str:
# mypyはsがNoneの可能性を指摘する
return s.upper() # エラー: "None"にはupperがない
mypyの出力例:
error_optional.py:5: error: Item "None" of "Optional[str]" has no attribute "upper" [union-attr]
修正例(ガードを入れる):
# ファイル名: fix_optional.py
from typing import Optional
def upper_or_default(s: Optional[str]) -> str:
if s is None:
return ""
return s.upper()
2) リストの要素型ミスマッチ
# ファイル名: error_list_type.py
names: list[str] = ["Alice", "Bob"]
names.append(123) # エラー: intはlist[str]に入れられない
修正例(型を統一する/入力を変換する):
# ファイル名: fix_list_type.py
names: list[str] = ["Alice", "Bob"]
names.append(str(123)) # 文字列に変換してから追加
3) 戻り値の取りうるパスが未カバー
# ファイル名: error_missing_return.py
def sign(n: int) -> str:
if n > 0:
return "plus"
elif n < 0:
return "minus"
# n == 0 の場合に戻り値がない → mypyが検出
修正例:
# ファイル名: fix_missing_return.py
def sign(n: int) -> str:
if n > 0:
return "plus"
elif n < 0:
return "minus"
else:
return "zero"
型エラーを無視しないコツ
基本方針は「ignoreを安易に使わない」です。
どうしても必要なら、範囲を最小化し、理由をコメントで残します。
# ファイル名: 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が推論した型を表示でき、デバッグに役立ちます。
# ファイル名: reveal_type_demo.py
from typing import Optional
def f(s: Optional[str]) -> None:
reveal_type(s) # mypy専用: "Optional[str]" と表示される
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.toml
かmypy.ini
を置きます。
最近はpyproject.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
# ファイル名: 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
モノレポなどではサブパッケージ単位でセクションを分けられます。
[tool.mypy.my_app.*]
disallow_untyped_defs = true
はじめての安全設定(推奨オプション)
最初から--strict
はハードルが高いので、実務で安全かつ導入しやすい最小セットを推奨します。
表: 初心者向けの実用オプション
オプション | 役割 | 効果 |
---|---|---|
show_error_codes | エラーコード表示 | 後でピンポイントに無視・検索がしやすい |
warn_unused_ignores | 使われないignoreを警告 | 無意味なignoreの蓄積を防止 |
warn_redundant_casts | 不要なcastを警告 | cast乱用の抑制 |
no_implicit_optional | TとOptional[T]の曖昧さを排除 | None安全性を高める |
check_untyped_defs | 注釈なし関数も内部をチェック | 漸進導入に有効 |
加えて、プロジェクトのPythonバージョンをpython_version
で明示しましょう。
少しずつ厳しくする方法(–strictへの道)
段階的強化で挫折を防ぎます。
以下を順に上げていくとスムーズです。
# ステップ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-*
パッケージを探します。
# 例: requests の型スタブ
pip install requests types-requests
# ファイル名: 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
を利用します。
TypedDict
やProtocol
などが補えます。
pip install typing-extensions
# ファイル名: 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
で一時的に対象外にするのも有効です。
# pyproject.toml
[tool.mypy]
exclude = [
"legacy/.*",
]
ignore-missing-importsの常用は非推奨です。
やむを得ず使う場合は、モジュール単位で限定的に適用し、のちに型スタブ導入で外す計画を立てます。
# 特定モジュールだけ
[[tool.mypy.overrides]]
module = "some_legacy_lib.*"
ignore_missing_imports = true
テストやCIでの自動チェック
テストと同列に型チェックをCIへ組み込みます。
失敗したらマージできない仕組みにするのが効果的です。
# ファイル名: .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
フックも便利です。
# ファイル名: .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はプロジェクト全体の厳密検査という役割分担が現実的です。
// ファイル名: .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
を含む型です。
使う前に必ずガードしましょう。
# ファイル名: 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)
- 関連記事:Noneとは?意味と使い方をやさしく解説
Any
Any
は「何でも通す」特別扱いで、バグがすり抜けやすいです。
境界(外部I/O)に閉じ込め、内部へ入れる前にパースして型を確定しましょう。
# ファイル名: 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の初期化と再代入
変数を後から値で埋める場合はユニオン型を明示します。
# ファイル名: none_init.py
token: str | None = None
# 後でセット
token = "abc123"
このパターンならmypyは「最初はNone、以降はstr」という意図を理解してくれます。
まとめ
型ヒント×mypyは、バグを「発生後に追う」から「発生前に止める」へ発想を転換させる実用的な武器です。
基礎として型ヒントの意味を押さえ、mypyで静的解析を回し、設定を段階的に厳しくしながら、CI・エディタ統合で常時チェックを自動化すると効果が最大化します。
OptionalやAny、Noneといった落とし穴も、ガード・パース・境界の設計で回避できます。
まずは小さく導入し、成功体験を積みながら適用範囲を広げていきましょう。