型ヒントは、Pythonのコードに「ここはどの型を想定しているか」を注記する仕組みです。
実行速度には影響せず、読みやすさや保守性、IDEの補完精度、静的解析の精度が向上します。
本記事では、基礎から書き方、実践パターン、そしてmypyによる静的型チェックまで、初学者の方でも段階的に理解できるように解説します。
Python型ヒントの基礎
型ヒントの目的と効果
型ヒントの主目的は、コードの意図を機械と人間の双方に明確に伝えることです。
具体的には、次のような効果があります。
- 可読性とドキュメント性が上がり、関数やデータ構造の使い方が明確になります。
- IDEやエディタが補完やジャンプ、警告をより高精度に行えます。
- mypyなどの静的型チェッカーで、実行前にバグの芽を早期発見できます。
- 大規模化した際の「破壊的変更」を検知しやすくなります。
型ヒントはあくまで注釈であり、標準のPython実行時に強制はされません。
これにより、動的言語の柔軟さを保ちつつ品質を高められます。
PEP 484とtypingの概要
Pythonの型ヒントはPEP 484で導入され、標準ライブラリのtyping
モジュールにより提供されています。
typing
は基本的な型(List
やDict
など)、合併型(Union
やOptional
)、汎用プログラミング用の型(TypeVar
やGeneric
)などを定義します。
- Python 3.9以降は、
list[int]
のように組み込みコレクション型に直接型引数を書けます(旧来はtyping.List[int]
)。 - Python 3.10以降は、合併型に
|
演算子が使えます(int | str
はUnion[int, str]
と同等)。 - 将来の参照が必要な場合や評価遅延による利点を得たい場合は、
from __future__ import annotations
の利用が有効です。
動的型付けとの違い
Pythonは動的型付けで、実行時に型が決まります。
型ヒントを書いても、Python自体はそれを強制しません。
このため、型が間違っていても実行されてしまうことがあります。
静的型チェッカーは、こうした問題を事前に見つけます。
# hint_demo.py
# 型ヒントはありますが、Pythonは実行時にこれを強制しません。
def repeat(s: str, n: int) -> str:
return s * n # 文字列sをn回繰り返す
print(repeat("ha", 3)) # 正しい呼び出し
print(repeat(3, "ha")) # 引数の並びが逆(論理的には誤り)でも実行は成功してしまう
hahaha
hahaha
mypyでチェックすると、間違いを発見できます。
mypy hint_demo.py
hint_demo.py:8: error: Argument 1 to "repeat" has incompatible type "int"; expected "str"
hint_demo.py:8: error: Argument 2 to "repeat" has incompatible type "str"; expected "int"
Found 2 errors in 1 file (checked 1 source file)
型注釈の書き方
変数の型ヒント
変数にはコロンで注釈を書きます。
初期化と同時でも、後から値を代入しても構いません。
# 変数への型注釈
count: int = 0
names: list[str] = ["Alice", "Bob"]
config: dict[str, int] = {"retries": 3}
pi: float # ここで型だけ注釈して後で代入
pi = 3.14159
# レガシーな書き方(コメント注釈)。新規では推奨されません。
data = [] # type: list[int]
定数や不変の意図
不変性の意図を示すにはtyping.Final
が使えます。
from typing import Final
API_VERSION: Final = "v1" # 上書きを意図しない
関数の引数と戻り値
関数の注釈は引数にコロン、戻り値に-> 型
を使います。
def greet(name: str, excited: bool = False) -> str:
"""挨拶文を生成する"""
base = f"Hello, {name}"
return base + "!" if excited else base
def total(*scores: int) -> int:
"""可変長引数の例"""
return sum(scores)
def debug(**flags: bool) -> None:
"""キーワード引数の例"""
print(flags)
print(greet("Alice"))
print(greet("Bob", excited=True))
print(total(10, 20, 30))
debug(verbose=True, dry_run=False)
Hello, Alice
Hello, Bob!
60
{'verbose': True, 'dry_run': False}
OptionalとUnion
Optional[T]
はT | None
の別名です。
Noneが来る可能性を型で表せます。
def parse_port(port: int | None) -> int:
"""ポート番号。Noneならデフォルト80を使う"""
if port is None: # Noneチェックで型が絞り込まれます(型ナローイング)
return 80
return port
複数の型のいずれかを受け付ける場合はUnion
(または|
)を使います。
def to_str(x: int | float | str) -> str:
return str(x)
コレクション型 List Dict Set Tuple
Python 3.9以降は組み込み型に直接型引数を書けます。
読み取り専用インターフェイスを表す場合はcollections.abc
由来の抽象型が便利です。
# 代表例
nums: list[int] = [1, 2, 3]
index: dict[str, int] = {"a": 1}
seen: set[str] = {"x", "y"}
point: tuple[float, float] = (35.0, 139.0)
# 長さ不定のタプル(同じ型が並ぶ)
values: tuple[int, ...] = (1, 2, 3, 4)
# 読み取り専用インターフェイス(反復専用など)を受ける引数
from collections.abc import Mapping, Sequence
def sum_all(xs: Sequence[int]) -> int: # listでもtupleでもOK
return sum(xs)
def lookup(key: str, m: Mapping[str, int]) -> int:
return m[key]
用途と書き方の対応表です。
用途 | 書き方(3.9+) | 旧来の書き方(typing) | 備考 |
---|---|---|---|
可変のリスト | list[T] | typing.List[T] | 関数引数はSequence[T]で受けると柔軟 |
辞書 | dict[K, V] | typing.Dict[K, V] | 読み取りのみならMapping[K, V] |
集合 | set[T] | typing.Set[T] | 変更しないならfrozenset[T] |
タプル | tuple[T1, T2] / tuple[T, …] | typing.Tuple[…] | 固定長と可変長で記法が異なる |
イテラブル | collections.abc.Iterable[T] | typing.Iterable[T] | 引数に広く受けるのに有用 |
Callable Any NoReturn
コールバック関数や高階関数にはCallable
を使います。
from typing import Callable
def operate(x: int, y: int, op: Callable[[int, int], int]) -> int:
"""2項演算を引数で受け取る"""
return op(x, y)
print(operate(2, 3, lambda a, b: a + b))
5
Any
は「型チェックを通す抜け道」です。
便利ですが、型安全性を下げるため乱用は避けます。
from typing import Any
payload: Any = {"id": 123} # Anyにすると何でも通ってしまう
payload = "oops" # これもエラーにならない
NoReturn
は「絶対に戻ってこない」関数(例: 例外送出のみ)に使います。
from typing import NoReturn
def fail(msg: str) -> NoReturn:
raise RuntimeError(msg)
typingの実用パターン
型エイリアス TypeAlias
複雑な型に名前を付けると、可読性が上がります。
from typing import TypeAlias
UserId: TypeAlias = int
GeoPoint: TypeAlias = tuple[float, float]
def locate(user_id: UserId) -> GeoPoint:
# 実装は仮の例
return (35.0, 139.0)
注: Python 3.12以降はtype UserId = int
の構文でもエイリアスを定義できます。
LiteralとEnumで値を制約
決まった文字列や数値のみを受け付けたい場合はLiteral
が便利です。
from typing import Literal
# methodは"GET"か"POST"のみ
def request(method: Literal["GET", "POST"], url: str) -> None:
print(method, url)
request("GET", "https://example.com")
# request("PATCH", "...") # mypyはエラーにします
Enum
を使うと、ランタイムでも値を厳密に扱えます。
from enum import Enum
class Method(Enum):
GET = "GET"
POST = "POST"
def request2(method: Method, url: str) -> None:
print(method.value, url)
request2(Method.GET, "https://example.com")
GET https://example.com
GET https://example.com
TypedDictとNamedTuple
JSON風の辞書に構造を与えるにはTypedDict
が適します。
from typing import TypedDict
class User(TypedDict):
id: int
name: str
is_active: bool
u: User = {"id": 1, "name": "Alice", "is_active": True} # 型整合性をチェックできる
一方、固定構造で位置引数アクセスが中心ならNamedTuple
が向きます。
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
p = Point(1.0, 2.0)
print(p.x, p.y)
1.0 2.0
dataclassと型ヒント
dataclass
はデータ保持用クラスを簡潔に定義でき、型ヒントと相性が良いです。
from dataclasses import dataclass, field
@dataclass
class Account:
id: int
name: str
tags: list[str] = field(default_factory=list) # ミュータブルはdefault_factoryで
active: bool = True
def describe(self) -> str:
return f"{self.id}:{self.name} ({'active' if self.active else 'inactive'})"
acc = Account(id=1, name="Alice")
acc.tags.append("premium")
print(acc)
print(acc.describe())
Account(id=1, name='Alice', tags=['premium'], active=True)
1:Alice (active)
ジェネリクス TypeVar Generic
型引数を受ける汎用クラスや関数は、TypeVar
とGeneric
で記述します。
from typing import Generic, TypeVar
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._data: list[T] = []
def push(self, item: T) -> None:
self._data.append(item)
def pop(self) -> T:
return self._data.pop()
ints = Stack[int]()
ints.push(10)
x = ints.pop() # xはintとして推論される
texts = Stack[str]()
texts.push("hello")
# texts.push(123) # mypyはエラーにします
print(x, texts.pop())
10 hello
mypyで静的型チェック
mypyのインストールと実行
プロジェクト用の仮想環境上にmypyをインストールし、ファイルやディレクトリを指定して実行します。
# 仮想環境の作成と有効化(例: Unix系)
python -m venv .venv
source .venv/bin/activate
# Windowsの場合は: .venv\Scripts\activate
# mypyのインストール
pip install mypy
# ファイル単位でチェック
mypy hint_demo.py
# ディレクトリ単位でチェック
mypy src/
エラーの読み方と対処
mypyの典型的なエラーと修正例を示します。
# wrong_return.py
def half(x: int) -> float:
if x % 2 == 0:
return x / 2
# 偶数以外で戻り値がない -> エラー
mypy wrong_return.py
wrong_return.py:5: error: Missing return statement
Found 1 error in 1 file (checked 1 source file)
def half(x: int) -> float:
return x / 2
別の例です。
None混入の扱いは、分岐で丁寧に絞ると解消します。
# maybe_len.py
def maybe_len(s: str | None) -> int:
# return len(s) # エラー: "None"の可能性
if s is None:
return 0
return len(s) # ここではsはstrに絞り込まれる
ときには、ライブラリの型が足りずにエラーが出ることがあります。
その場合は一時的に# type: ignore[error-code]
で抑制し、後述のサードパーティ型定義の導入を検討します。
型推論の確認にはreveal_type
が役立ちます(mypy専用の疑似関数です)。
# reveal_demo.py
def f(x: int | None) -> int:
if x is None:
return 0
reveal_type(x) # mypy: Revealed type is "builtins.int"
return x
mypy reveal_demo.py
reveal_demo.py:4: note: Revealed type is "builtins.int"
Success: no issues found in 1 source file
pyproject.tomlで設定
プロジェクト全体の設定はpyproject.toml
に記述できます。
最初は緩めに、有効化できる範囲から始めるのが現実的です。
[tool.mypy]
python_version = "3.11"
# まずは外部ライブラリ起因のエラーを黙らせる
ignore_missing_imports = true
# 未使用の # type: ignore を警告
warn_unused_ignores = true
# 未型注釈の関数定義を許容(段階的に厳しくする)
disallow_untyped_defs = false
# Noneの混入を厳密に扱う
no_implicit_optional = true
# モジュール単位で上書き
[[tool.mypy.overrides]]
module = ["myapp.core.*"]
disallow_untyped_defs = true # 中核部分は厳しくする
より厳格にする段階で、次のオプションも検討します。
check_untyped_defs = true
未注釈関数も型チェックwarn_redundant_casts = true
不要なcastを警告warn_return_any = true
Anyの戻り値を警告strict = true
一括で多くの厳格オプションを有効化
段階的な導入方法
初めからプロジェクト全体を厳格化すると挫折しやすいです。
次の順序だと進めやすくなります。
- 新規コードの関数シグネチャ(引数と戻り値)にだけ注釈を付ける。
- 中核モジュールから
disallow_untyped_defs = true
を適用し、徐々に対象を広げる。 - 変数や属性にも注釈を追加し、
no_implicit_optional = true
などの厳格化を進める。 - 既存コードのエラーは、まず
ignore_missing_imports = true
やピンポイントの# type: ignore[code]
で抑え、時間を見つけて根本対応する。
サードパーティの型定義
多くのライブラリは型情報を同梱していますが、未同梱の場合はPEP 561準拠のスタブパッケージを追加できます。
# 例: requestsの型スタブ
pip install types-requests
mypyは標準のtypeshedと、インストール済みスタブパッケージから型を解決します。
新しめの型機能を古いPythonで使いたい場合は、typing_extensions
の導入も有効です。
pip install typing_extensions
TypeAlias
やLiteralString
など、標準化前後の機能を補完してくれます。
まとめ
型ヒントは、Pythonの柔軟さを損なわずにコードの意図を明確化し、開発体験と品質を大きく改善します。
まずは関数の引数と戻り値から注釈を始め、list[int]
やdict[str, int]
などの基本的なコレクション型、Optional
や合併型|
を使いこなしてください。
次の段階でTypedDict
やdataclass
、ジェネリクスなどの実用パターンを取り入れると、モデルやAPI層の堅牢性が高まります。
最後にmypyを導入し、pyproject.toml
で段階的に厳格化していけば、実行前に多くの不具合を防げるようになります。
型ヒントは「書けば終わり」ではなく、設計の意図を共有し続けるためのドキュメントでもあります。
小さく始めて、着実に広げていきましょう。