閉じる

Python型ヒントの書き方と使い方: typingとmypyまで

型ヒントは、Pythonのコードに「ここはどの型を想定しているか」を注記する仕組みです。

実行速度には影響せず、読みやすさや保守性、IDEの補完精度、静的解析の精度が向上します。

本記事では、基礎から書き方、実践パターン、そしてmypyによる静的型チェックまで、初学者の方でも段階的に理解できるように解説します。

Python型ヒントの基礎

型ヒントの目的と効果

型ヒントの主目的は、コードの意図を機械と人間の双方に明確に伝えることです。

具体的には、次のような効果があります。

  • 可読性とドキュメント性が上がり、関数やデータ構造の使い方が明確になります。
  • IDEやエディタが補完やジャンプ、警告をより高精度に行えます。
  • mypyなどの静的型チェッカーで、実行前にバグの芽を早期発見できます。
  • 大規模化した際の「破壊的変更」を検知しやすくなります。

型ヒントはあくまで注釈であり、標準のPython実行時に強制はされません。

これにより、動的言語の柔軟さを保ちつつ品質を高められます。

PEP 484とtypingの概要

Pythonの型ヒントはPEP 484で導入され、標準ライブラリのtypingモジュールにより提供されています。

typingは基本的な型(ListDictなど)、合併型(UnionOptional)、汎用プログラミング用の型(TypeVarGeneric)などを定義します。

  • Python 3.9以降は、list[int]のように組み込みコレクション型に直接型引数を書けます(旧来はtyping.List[int])。
  • Python 3.10以降は、合併型に|演算子が使えます(int | strUnion[int, str]と同等)。
  • 将来の参照が必要な場合や評価遅延による利点を得たい場合は、from __future__ import annotationsの利用が有効です。

動的型付けとの違い

Pythonは動的型付けで、実行時に型が決まります。

型ヒントを書いても、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でチェックすると、間違いを発見できます。

Shell
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)

型注釈の書き方

変数の型ヒント

変数にはコロンで注釈を書きます。

初期化と同時でも、後から値を代入しても構いません。

Python
# 変数への型注釈
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が使えます。

Python
from typing import Final

API_VERSION: Final = "v1"  # 上書きを意図しない

関数の引数と戻り値

関数の注釈は引数にコロン、戻り値に-> 型を使います。

Python
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)
Python
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が来る可能性を型で表せます。

Python
def parse_port(port: int | None) -> int:
    """ポート番号。Noneならデフォルト80を使う"""
    if port is None:       # Noneチェックで型が絞り込まれます(型ナローイング)
        return 80
    return port

複数の型のいずれかを受け付ける場合はUnion(または|)を使います。

Python
def to_str(x: int | float | str) -> str:
    return str(x)

コレクション型 List Dict Set Tuple

Python 3.9以降は組み込み型に直接型引数を書けます。

読み取り専用インターフェイスを表す場合はcollections.abc由来の抽象型が便利です。

Python
# 代表例
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を使います。

Python
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は「型チェックを通す抜け道」です。

便利ですが、型安全性を下げるため乱用は避けます。

Python
from typing import Any

payload: Any = {"id": 123}   # Anyにすると何でも通ってしまう
payload = "oops"              # これもエラーにならない

NoReturnは「絶対に戻ってこない」関数(例: 例外送出のみ)に使います。

Python
from typing import NoReturn

def fail(msg: str) -> NoReturn:
    raise RuntimeError(msg)

typingの実用パターン

型エイリアス TypeAlias

複雑な型に名前を付けると、可読性が上がります。

Python
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が便利です。

Python
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を使うと、ランタイムでも値を厳密に扱えます。

Python
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が適します。

Python
from typing import TypedDict

class User(TypedDict):
    id: int
    name: str
    is_active: bool

u: User = {"id": 1, "name": "Alice", "is_active": True}  # 型整合性をチェックできる

一方、固定構造で位置引数アクセスが中心ならNamedTupleが向きます。

Python
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はデータ保持用クラスを簡潔に定義でき、型ヒントと相性が良いです。

Python
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

型引数を受ける汎用クラスや関数は、TypeVarGenericで記述します。

Python
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をインストールし、ファイルやディレクトリを指定して実行します。

Shell
# 仮想環境の作成と有効化(例: Unix系)
python -m venv .venv
source .venv/bin/activate

# Windowsの場合は: .venv\Scripts\activate

# mypyのインストール
pip install mypy

# ファイル単位でチェック
mypy hint_demo.py

# ディレクトリ単位でチェック
mypy src/

エラーの読み方と対処

mypyの典型的なエラーと修正例を示します。

Python
# wrong_return.py
def half(x: int) -> float:
    if x % 2 == 0:
        return x / 2
    # 偶数以外で戻り値がない -> エラー
Shell
mypy wrong_return.py
実行結果
wrong_return.py:5: error: Missing return statement
Found 1 error in 1 file (checked 1 source file)

Python簡単な修正
def half(x: int) -> float:
    return x / 2

別の例です。

None混入の扱いは、分岐で丁寧に絞ると解消します。

Python
# 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専用の疑似関数です)。

Python
# 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
Shell
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に記述できます。

最初は緩めに、有効化できる範囲から始めるのが現実的です。

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 一括で多くの厳格オプションを有効化

段階的な導入方法

初めからプロジェクト全体を厳格化すると挫折しやすいです。

次の順序だと進めやすくなります。

  1. 新規コードの関数シグネチャ(引数と戻り値)にだけ注釈を付ける。
  2. 中核モジュールからdisallow_untyped_defs = trueを適用し、徐々に対象を広げる。
  3. 変数や属性にも注釈を追加し、no_implicit_optional = trueなどの厳格化を進める。
  4. 既存コードのエラーは、まずignore_missing_imports = trueやピンポイントの# type: ignore[code]で抑え、時間を見つけて根本対応する。

サードパーティの型定義

多くのライブラリは型情報を同梱していますが、未同梱の場合はPEP 561準拠のスタブパッケージを追加できます。

Shell
# 例: requestsの型スタブ
pip install types-requests

mypyは標準のtypeshedと、インストール済みスタブパッケージから型を解決します。

新しめの型機能を古いPythonで使いたい場合は、typing_extensionsの導入も有効です。

Shell
pip install typing_extensions

TypeAliasLiteralStringなど、標準化前後の機能を補完してくれます。

まとめ

型ヒントは、Pythonの柔軟さを損なわずにコードの意図を明確化し、開発体験と品質を大きく改善します。

まずは関数の引数と戻り値から注釈を始め、list[int]dict[str, int]などの基本的なコレクション型、Optionalや合併型|を使いこなしてください。

次の段階でTypedDictdataclass、ジェネリクスなどの実用パターンを取り入れると、モデルやAPI層の堅牢性が高まります。

最後にmypyを導入し、pyproject.tomlで段階的に厳格化していけば、実行前に多くの不具合を防げるようになります。

型ヒントは「書けば終わり」ではなく、設計の意図を共有し続けるためのドキュメントでもあります。

小さく始めて、着実に広げていきましょう。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!