Pythonは動的型付け言語ですが、Python 3.5以降で導入された型ヒントにより、静的型付けの恩恵を受けられるようになりました。型ヒントはコードの可読性を高め、IDEの補完を強化し、バグを早期に発見できます。
なぜ型ヒントを使うのか
動的型付けは柔軟ですが、大規模なコードベースでは問題になることがあります:
# 型ヒントなし - 何を渡せばいいかわからない
def process_data(data, options):
# dataは何型?optionsはdict?何のキーがある?
pass
# 型ヒントあり - 意図が明確
def process_data(data: list[dict], options: ProcessOptions) -> Result:
pass
型ヒントのメリット:
| メリット | 説明 |
|---|---|
| ドキュメント | 関数のシグネチャが仕様書になる |
| IDE補完 | 正確な補完候補とエラー検出 |
| バグ検出 | mypyで実行前にエラーを発見 |
| リファクタリング | 型の不整合を即座に検出 |
基本的な型アノテーション
変数と関数
# 変数アノテーション
name: str = "Python"
count: int = 42
ratio: float = 3.14
is_valid: bool = True
# 関数アノテーション
def greet(name: str) -> str:
return f"Hello, {name}!"
# 引数のデフォルト値
def connect(host: str, port: int = 8080) -> None:
pass
コレクション型
Python 3.9以降は組み込み型を直接使用できます:
# リスト
numbers: list[int] = [1, 2, 3]
# 辞書
scores: dict[str, int] = {"alice": 100, "bob": 85}
# セット
unique_ids: set[str] = {"id1", "id2"}
# タプル(固定長)
point: tuple[int, int] = (10, 20)
# タプル(可変長)
values: tuple[int, ...] = (1, 2, 3, 4, 5)
Python 3.8以前ではtypingモジュールを使用:
from typing import List, Dict, Set, Tuple
numbers: List[int] = [1, 2, 3]
scores: Dict[str, int] = {"alice": 100}
OptionalとUnion
None許容型
値がNoneになりうる場合はOptionalを使用:
from typing import Optional
def find_user(user_id: int) -> Optional[User]:
"""ユーザーが見つからない場合はNoneを返す"""
user = db.get(user_id)
return user # User or None
# Python 3.10以降は | 構文も使用可能
def find_user(user_id: int) -> User | None:
pass
複数の型を許容
from typing import Union
# Python 3.9以前
def process(value: Union[int, str]) -> str:
return str(value)
# Python 3.10以降
def process(value: int | str) -> str:
return str(value)
ジェネリクス
TypeVar
型パラメータを定義して、関数やクラスを汎用的にします:
from typing import TypeVar
T = TypeVar('T')
def first(items: list[T]) -> T:
"""リストの最初の要素を返す"""
return items[0]
# 使用時に型が推論される
num = first([1, 2, 3]) # int
name = first(["a", "b"]) # str
型の制約
from typing import TypeVar
# 特定の型に制限
Number = TypeVar('Number', int, float)
def add(a: Number, b: Number) -> Number:
return a + b
# 上限を指定
from typing import TypeVar
Comparable = TypeVar('Comparable', bound='SupportsLessThan')
ジェネリッククラス
from typing import Generic, TypeVar
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# 使用
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop()
Callable
関数を引数として受け取る場合:
from typing import Callable
# 引数なし、戻り値なし
def run_task(task: Callable[[], None]) -> None:
task()
# 引数あり
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# 可変長引数
def decorator(func: Callable[..., T]) -> Callable[..., T]:
def wrapper(*args, **kwargs) -> T:
return func(*args, **kwargs)
return wrapper
TypedDict
辞書のキーと値の型を厳密に定義:
from typing import TypedDict
class UserDict(TypedDict):
name: str
age: int
email: str
def create_user(data: UserDict) -> None:
print(data["name"]) # strとして認識
# 正しい使用
user: UserDict = {"name": "Alice", "age": 30, "email": "alice@example.com"}
# オプショナルなキー
class ConfigDict(TypedDict, total=False):
debug: bool
timeout: int
Protocol(構造的部分型)
ダックタイピングを型安全に:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(shape: Drawable) -> None:
shape.draw()
# CircleとSquareはDrawableを明示的に継承していないが、
# draw()メソッドを持つため、Drawableとして扱える
render(Circle()) # OK
render(Square()) # OK
ランタイムチェック可能なProtocol
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sized(Protocol):
def __len__(self) -> int: ...
# isinstance()でチェック可能
def process(obj: object) -> None:
if isinstance(obj, Sized):
print(f"Length: {len(obj)}")
型エイリアス
複雑な型を簡潔に:
from typing import TypeAlias
# シンプルなエイリアス
UserId: TypeAlias = int
Coordinates: TypeAlias = tuple[float, float]
# 複雑な型をシンプルに
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
def parse_json(text: str) -> JsonValue:
import json
return json.loads(text)
Literal
特定の値のみを許容:
from typing import Literal
def set_mode(mode: Literal["read", "write", "append"]) -> None:
pass
set_mode("read") # OK
set_mode("delete") # エラー
# 複数の値
Status = Literal["pending", "active", "completed"]
mypyとの連携
mypyのインストールと実行
# インストール
pip install mypy
# 単一ファイルをチェック
mypy script.py
# ディレクトリ全体をチェック
mypy src/
# 厳格モード
mypy --strict src/
設定ファイル(pyproject.toml)
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
# 特定のモジュールを除外
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
よくあるmypyエラーと対処法
# エラー: 型が不明
result = [] # error: Need type annotation for 'result'
result: list[int] = [] # OK
# エラー: Noneの可能性
def get_name() -> str | None:
return None
name = get_name()
print(name.upper()) # error: Item "None" has no attribute "upper"
# 対処: Noneチェック
if name is not None:
print(name.upper()) # OK
# または assert
assert name is not None
print(name.upper()) # OK
型を無視する
# 特定の行を無視
result = some_untyped_function() # type: ignore
# 特定のエラーコードのみ無視
result = func() # type: ignore[no-untyped-call]
# 理由を記載(推奨)
result = legacy_func() # type: ignore[no-untyped-call] # TODO: Add types to legacy_func
実践的なパターン
ファクトリ関数
from typing import Type, TypeVar
T = TypeVar('T', bound='BaseModel')
def create_model(model_class: Type[T], data: dict) -> T:
return model_class(**data)
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
user = create_model(User, {"name": "Alice", "age": 30})
# userはUser型として推論される
デコレータ
from typing import Callable, ParamSpec, TypeVar
from functools import wraps
P = ParamSpec('P')
R = TypeVar('R')
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
result = add(1, 2) # 型情報が保持される
オーバーロード
from typing import overload
@overload
def process(value: int) -> str: ...
@overload
def process(value: str) -> int: ...
def process(value: int | str) -> str | int:
if isinstance(value, int):
return str(value)
return int(value)
# 呼び出し側で正確な型が推論される
x = process(42) # str
y = process("42") # int
まとめ
Pythonの型ヒントは、コードの品質と保守性を大幅に向上させます:
flowchart LR
subgraph Basic["基本"]
B1["変数・関数アノテーション"]
B2["Optional/Union"]
B3["コレクション型"]
end
subgraph Advanced["応用"]
A1["ジェネリクス"]
A2["Protocol"]
A3["TypedDict"]
end
subgraph Tools["ツール"]
T1["mypy"]
T2["IDE補完"]
T3["リファクタリング"]
end
Basic --> Advanced --> Tools
style Basic fill:#3b82f6,color:#fff
style Advanced fill:#8b5cf6,color:#fff
style Tools fill:#22c55e,color:#fff
| 機能 | ユースケース |
|---|---|
Optional[T] |
Noneを返す可能性がある |
Union[A, B] |
複数の型を許容 |
TypeVar |
ジェネリックな関数・クラス |
Protocol |
ダックタイピングの型安全化 |
TypedDict |
辞書のスキーマ定義 |
Literal |
特定の値のみ許容 |
主要な原則:
- 段階的に導入: 新しいコードから型ヒントを追加
- mypyを活用: CI/CDに組み込んで自動チェック
- Protocolを活用: 継承よりも構造的部分型を優先
- 過度に複雑にしない: 読みやすさとのバランスを取る
型ヒントは「ドキュメント」「検証」「補完」の3つの価値を同時に提供します。最初は基本的なアノテーションから始め、徐々に高度な機能を取り入れていきましょう。