はじめに
Pythonは動的型付け言語ですが、Python 3.5 で導入された**型ヒント(Type Hints)**により、静的な型情報をコードに付与できるようになりました。型ヒントは実行時の動作に影響しませんが、以下のメリットがあります。
- 可読性の向上: 関数の引数と戻り値の型が一目で分かる
- IDEサポートの強化: 自動補完やリファクタリングの精度が向上する
- バグの早期発見: mypy 等の静的型チェッカーで実行前にエラーを検出できる
- ドキュメントとしての役割: コードが自己文書化される
本記事では、型ヒントの基礎から mypy を使った静的型チェックまでを実践的に解説します。
基本的な型ヒント
変数の型ヒント
Python 3.6+ では変数にも型ヒントを付けられます。
name: str = "hello"
age: int = 30
height: float = 175.5
is_active: bool = True
型ヒントと異なる値を代入してもランタイムエラーにはなりませんが、mypy が警告を出します。
name: str = 123 # mypy: Incompatible types in assignment
関数の型ヒント
関数の引数と戻り値に型を付けるのが最も一般的な使い方です。
def greet(name: str) -> str:
return f"Hello, {name}"
def add(a: int, b: int) -> int:
return a + b
戻り値がない関数
戻り値がない関数には -> None を付けます。
def log_message(message: str) -> None:
print(f"[LOG] {message}")
デフォルト引数
デフォルト引数がある場合は、型ヒントの後に = で記述します。
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}"
コレクション型
Python 3.9+ のビルトインジェネリクス
Python 3.9 以降では、組み込み型をそのままジェネリクスとして使えます。
# Python 3.9+
names: list[str] = ["Alice", "Bob"]
scores: dict[str, int] = {"Alice": 95, "Bob": 87}
coordinates: tuple[float, float] = (35.68, 139.76)
unique_tags: set[str] = {"python", "typing"}
可変長タプルには ... を使います。
# 任意の長さのintタプル
numbers: tuple[int, ...] = (1, 2, 3, 4, 5)
Python 3.8 以前(typing モジュール)
Python 3.8 以前では typing モジュールからインポートする必要があります。
from typing import List, Dict, Tuple, Set
names: List[str] = ["Alice", "Bob"]
scores: Dict[str, int] = {"Alice": 95}
coordinates: Tuple[float, float] = (35.68, 139.76)
unique_tags: Set[str] = {"python", "typing"}
推奨: Python 3.9+ ではビルトイン型(
list,dict,tuple,set)を使いましょう。typing.List等は将来的に非推奨になります。
ネストしたコレクション
コレクション型はネストできます。
# 文字列のリストを値に持つ辞書
user_tags: dict[str, list[str]] = {
"alice": ["python", "ml"],
"bob": ["go", "docker"],
}
# タプルのリスト
points: list[tuple[int, int]] = [(0, 0), (1, 2), (3, 4)]
Optional と Union
Optional
値が None になり得る場合に使います。
from typing import Optional
# Python 3.9 以前
def find_user(user_id: int) -> Optional[str]:
if user_id == 1:
return "Alice"
return None
Python 3.10 以降では | 演算子で簡潔に書けます。
# Python 3.10+
def find_user(user_id: int) -> str | None:
if user_id == 1:
return "Alice"
return None
Union
複数の型を受け入れる場合に使います。
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)
Optional は Union の糖衣構文
Optional[X] は Union[X, None] と完全に等価です。
# 以下はすべて等価
from typing import Optional, Union
x: Optional[str]
x: Union[str, None]
x: str | None # Python 3.10+
TypedDict と dataclass
TypedDict
辞書のキーと値の型を定義できます。APIレスポンスや設定ファイルの型付けに便利です。
from typing import TypedDict
class UserProfile(TypedDict):
name: str
age: int
email: str
is_active: bool
def get_user() -> UserProfile:
return {
"name": "Alice",
"age": 30,
"email": "alice@example.com",
"is_active": True,
}
user = get_user()
print(user["name"]) # "Alice" — IDEが補完してくれる
オプショナルなキーがある場合は total=False またはキーごとの NotRequired(3.11+)を使います。
from typing import TypedDict, NotRequired # Python 3.11+
class UserProfile(TypedDict):
name: str
age: int
nickname: NotRequired[str] # 省略可能
dataclass
構造化データには dataclass が適しています。TypedDict と異なり、属性アクセス(.name)ができます。
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str
is_active: bool = True
user = User(name="Alice", age=30, email="alice@example.com")
print(user.name) # "Alice"
print(user) # User(name='Alice', age=30, email='alice@example.com', is_active=True)
frozen=True でイミュータブルにできます。
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
p.x = 3.0 # FrozenInstanceError
TypedDict vs dataclass の使い分け
| 観点 | TypedDict | dataclass |
|---|---|---|
| データ構造 | dict | クラスインスタンス |
| アクセス方法 | d["key"] | obj.attr |
| JSON互換性 | そのまま dict として扱える | 変換が必要 |
| イミュータブル | 不可 | frozen=True で可能 |
| 用途 | API応答、設定 | ドメインモデル、値オブジェクト |
ジェネリクス(Generics)
TypeVar によるジェネリック関数
型を抽象化して汎用的な関数を作れます。
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# 型推論が働く
name = first(["Alice", "Bob"]) # str
number = first([1, 2, 3]) # int
TypeVar に制約を付けることもできます。
from typing import TypeVar
Number = TypeVar("Number", int, float)
def double(x: Number) -> Number:
return x * 2
double(5) # OK: int
double(3.14) # OK: float
double("hi") # mypy error
Python 3.12+ の新しい構文
Python 3.12 では、TypeVar を明示的に定義する必要がなくなりました。
# Python 3.12+
def first[T](items: list[T]) -> T:
return items[0]
ジェネリッククラス
from typing import TypeVar, Generic
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:
if not self._items:
raise IndexError("Stack is empty")
return self._items.pop()
def is_empty(self) -> bool:
return len(self._items) == 0
# 使用時に型が確定する
int_stack = Stack[int]()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop() # 2
str_stack = Stack[str]()
str_stack.push("hello")
Python 3.12+ ではクラスも新しい構文で書けます。
# Python 3.12+
class Stack[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()
Protocol(構造的部分型)
ダックタイピングに型安全性を
Pythonの「ダックタイピング」の思想を活かしつつ、型チェックの恩恵を受けられるのが Protocol(Python 3.8+)です。
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing a circle"
class Square:
def draw(self) -> str:
return "Drawing a square"
def render(shape: Drawable) -> None:
print(shape.draw())
# CircleもSquareもDrawableを明示的に継承していないが、
# draw() メソッドを持つので型チェックを通過する
render(Circle()) # OK
render(Square()) # OK
Protocol と ABC の違い
Protocol は**構造的部分型(structural subtyping)**で、明示的な継承が不要です。
from abc import ABC, abstractmethod
from typing import Protocol
# ABC: 明示的に継承が必要(名目的部分型)
class DrawableABC(ABC):
@abstractmethod
def draw(self) -> str:
...
class CircleABC(DrawableABC): # 継承が必要
def draw(self) -> str:
return "circle"
# Protocol: 継承不要(構造的部分型)
class DrawableProtocol(Protocol):
def draw(self) -> str:
...
class CircleProtocol: # 継承不要。draw()があればOK
def draw(self) -> str:
return "circle"
実用例:ロガーインターフェース
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None:
...
class ConsoleLogger:
def log(self, message: str) -> None:
print(f"[CONSOLE] {message}")
class FileLogger:
def __init__(self, path: str) -> None:
self.path = path
def log(self, message: str) -> None:
with open(self.path, "a") as f:
f.write(f"{message}\n")
def process_data(data: list[int], logger: Logger) -> int:
logger.log(f"Processing {len(data)} items")
result = sum(data)
logger.log(f"Result: {result}")
return result
# どちらも Logger Protocol を満たす
process_data([1, 2, 3], ConsoleLogger())
process_data([1, 2, 3], FileLogger("/tmp/app.log"))
mypyによる静的型チェック
インストールと基本的な使い方
pip install mypy
# 単一ファイルのチェック
mypy script.py
# ディレクトリ全体のチェック
mypy src/
# パッケージのチェック
mypy -p mypackage
mypy が検出するエラーの例
# example.py
def greet(name: str) -> str:
return f"Hello, {name}"
result: int = greet("Alice") # error: Incompatible types in assignment
greet(123) # error: Argument 1 has incompatible type "int"
$ mypy example.py
example.py:4: error: Incompatible types in assignment
(expression has type "str", variable has type "int")
example.py:5: error: Argument 1 to "greet" has incompatible type "int";
expected "str"
Found 2 errors in 1 file (checked 1 source file)
設定ファイル
pyproject.toml または mypy.ini で設定を管理します。
# pyproject.toml
[tool.mypy]
python_version = "3.12"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
strict_equality = true
# サードパーティライブラリの型スタブがない場合
[[tool.mypy.overrides]]
module = ["some_untyped_library.*"]
ignore_missing_imports = true
–strict モード
--strict は最も厳格なチェックを有効にします。新規プロジェクトでの採用を推奨します。
mypy --strict src/
--strict が有効にする主なオプション:
| オプション | 説明 |
|---|---|
disallow_untyped_defs | 型ヒントなし関数を禁止 |
disallow_any_generics | list を list[int] のように要求 |
warn_return_any | Any を返す関数に警告 |
no_implicit_reexport | 明示的でない再エクスポートを禁止 |
strict_equality | 型が異なる == 比較に警告 |
よくあるエラーと対処法
# 1. Missing return statement
def get_name(user_id: int) -> str:
if user_id == 1:
return "Alice"
# error: Missing return statement
# 修正: return "" または raise ValueError()
# 2. Incompatible types in assignment
data: list[int] = []
data.append("hello") # error
# 修正: data: list[int | str] = [] または data.append(123)
# 3. Item "None" of "Optional[str]" has no attribute "upper"
def process(name: str | None) -> str:
return name.upper() # error: name が None の可能性
# 修正: None チェックを追加
# if name is None:
# return ""
# return name.upper()
# 4. type: ignore で特定行を除外(最終手段)
result = some_untyped_function() # type: ignore[no-untyped-call]
型ヒントのベストプラクティス
| プラクティス | 説明 |
|---|---|
| 公開APIから始める | まずは関数のシグネチャ(引数と戻り値)に型を付ける |
Any を避ける | Any は型チェックを無効化する。具体的な型や object を使う |
Optional を明示する | None を返す可能性がある場合は必ず Optional / X | None を付ける |
TypeVar に制約を付ける | 無制約の TypeVar よりも bound や制約型を使う |
| 段階的に導入する | 既存プロジェクトには --disallow-untyped-defs を段階的に適用 |
type: ignore にコメントを | 理由を添える: # type: ignore[arg-type] # legacy API |
| CI に mypy を組み込む | pre-commit や CI パイプラインで自動チェックする |
py.typed マーカーを配置 | ライブラリを公開する場合は py.typed ファイルを含める |
関連記事
- Pythonデコレータの仕組みと実践パターン - デコレータに型ヒントを付ける実践的なパターンを解説しています。
- Python asyncio入門 - 非同期関数の型ヒント(
Coroutine,Awaitable)について解説しています。 - Python正規表現実践ガイド -
reモジュールの型ヒント(re.Match,re.Pattern)の活用を紹介しています。 - Pythonのloggingモジュール実践ガイド - Protocol パターンを使ったロガーインターフェースの設計に関連します。
参考文献
- Python公式ドキュメント: typing
- mypy公式ドキュメント
- PEP 484 – Type Hints
- PEP 604 – Allow writing union types as X | Y
- PEP 695 – Type Parameter Syntax
- PEP 544 – Protocols: Structural subtyping
関連ツール
- DevToolBox - 開発者向け無料ツール集 - JSON整形、正規表現テスターなど85種類以上の開発者向けツール
- CalcBox - 暮らしの計算ツール - 統計計算、周波数変換など61種類以上の計算ツール