Python型ヒント実践ガイド:基礎からmypy活用まで

Pythonの型ヒント(Type Hints)を基礎から実践まで解説。typing モジュール、ジェネリクス、Protocol、mypyによる静的型チェックまで網羅します。

はじめに

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 の使い分け

観点TypedDictdataclass
データ構造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_genericslistlist[int] のように要求
warn_return_anyAny を返す関数に警告
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 ファイルを含める

関連記事

参考文献


関連ツール