Pythonのtypingについて改めて調べてみた

に公開

今回はPythonのtypingについて今までちゃんと調べずに使っていたこともあったので調べてみることにしました。

typingとは?

Pythonでデフォルトで利用可能なtypingは、Pythonにおけるタイプヒントをサポートする目的で実装されています。以下が公式ページになります。注釈にも書いてありますが、一般的はPythonランタイムは関数や変数のアノテーションについて強制することはありません。あくまで型アノテーションは明示的に型を示すことでセルフドキュメントとして機能します。

※ mypyなどの静的型チェッカーを利用するさいはタイプヒントが必要です。基本的にはmypyを利用しない場合もアノテーションはつけるようにしましょう。本記事では、以下のリンクの内容を要約したものとなります。

https://docs.python.org/ja/3.13/library/typing.html

代表的なものを解説します!

型エイリアス

型エイリアスはtypeを利用して定義され、TypeAliasTypeインスタンスが生成されます。例えば以下のように使います。なお、この記法は3.12より利用できます。

type Vector = list[float]

def sum_vector(vector: Vector) -> float:
  return sum(vector)

なお、typing.TypeAliasを利用することで明示的に指定することもできます。

from typing import TypeAlias

Vector: TypeAlias = list[float]

NewType

元の型と異なる型として定義したい場合はtyping.NewTypeを使います。

from typing import NewType

UserId = NewType("UserId", int)


def get_user_name(user_id: UserId) -> str:
  return ""


get_user_name(UserId(1))  # OK: UserId型を引数として指定しているため静的型チェックに成功する
get_user_name(1)  # NG: int型を引数として指定しているため失敗する

呼び出し可能オブジェクト

例えば関数などのような呼び出し可能オブジェクトをアノテーションする場合はtyping.Callableまたはcollections.abc.Callableを利用します。例えば以下のようになります。

from typing import Callable


def func(callback: Callable[[int], str]):
    ...
    callback(...)

Callable[[A], B]において、Aには引数の型、Bには戻り値の型を指定します。

ジェネリクス

コンテナに含まれるオブジェクトに関する型情報はcollections.abcのなかのMappingSequenceなどがあります。例えば、int型は確定しているがコンテナのデータは確定していない場合、つまりlistもあればtupleもある場合、以下のように記載できます。

from collections.abc import Sequence

def calculate_sum(items: Sequence[int]) -> int:
  return sum(items)

ジェネレータ

ジェネレータを定義するにはtyping.Generatorを用います。利用方法位は以下になります。

from typing import Generator


def stream() -> Generator[int, float, str]:
  ...

ここでGeneratorの中の設定内容ですが最初から順番にyieldで返されるデータ型sendで送られるデータ型そしてreturnで戻されるデータ型を指定します。

ユーザ定義のジェネリック型

ユーザ定義のジェネリック型を定義するには以下のようにすることで実現できます。

class Hoge[T]:
    def __init__(self, value: T):
        self.value = value


hoge = Hoge[int](10)

これ以外にも、typing.GeneritcTypeVarを利用して以下のようにかくこともできます。

from typing import TypeVar, Generic

T = TypeVar("T")

class Hoge(Generic[T]):
    ...

Any型

どのような値が入ることも受け入れるためにtyping.Anyが用意されています。利用方法は以下です。

from typing import Any


def func(value: Any) -> Any:
    ...

ただし、Anyだとどのようなデータを指定するべきか判定ができないので、基本的には利用しない方針がいいかと思います。

Never

typing.Neverというものがあり、これを指定された関数はreturnがない、つまりエラーやsys.exitなどが実行されることを明記できます。

from typing import Never


def stop() -> Never:
    raise RuntimeError()

TypeAlias

先ほども出てきましたが、データ型を定義するために明示的に指定する方法としてTypeAliasがあります。

from typing import TypeAlias

Vector: TypeAlias = list[float]

特殊形式

まず初めはユニオン型になります。ユニオン型は複数の候補がありうるデータ型になっており、従来はtyping.Unionが利用されていましたが、最近のバージョンではパイプ(|)を利用することで記載できます。

from typing import Union


def func1(x: Union[int, str]):
    ...

def func2(x: int | str):
    ...

次はオプショナルになります。オプショナルは指定した型またはNoneとなるものです。従来はtyping.Optionalが主流でしたが、今はUnion + None という感じでかけます。つまり以下のようになります。

from typing import Optional


def func1(x: Optional[int]):
    ...

def func2(x: int | None):
    ...

3つ目はLiteralです。これは複数の値の候補を明示する時に利用できるものです。例えば以下のように利用します。

from typing import Literal

type Mode = Literal["r", "w", "rb", "wb"]

def write_file(filepath: str, mode: Mode):
    ...


def write_file("hoge.txt", "w")

Annotated

typing.Annotatedはデータ型にコンテキスト特有のメタデータを不要する時に利用されます。メタデータを与えることで、静的分析の際に利用されます。

例えばベースはint型ですがその値の取りうる範囲をメタデータとして与えたい場合、以下のように記載できます。

from typing import Annotated
from dataclasses import dataclass


@dataclass
class IntValueRange:
    lo: int
    hi: int


RangedInt = Annotated[int, ValueRange(-10, 10)]

このようにすることで、RangeIntは-10から10の値を取りうるint型であると定義できます。なお、基本的にインタプリタには無視される情報であり、FastAPIやPydanticなど一部のライブラリなどでは解釈されます。

TypedDict

TypedDictは全てのインスタンスでキーと値が完全に固定されている場合に利用すると便利です。例えば、以下のようにキーxはint、キーyはfloatの値を必ず取ると明示する場合、以下のようにします。

from typing import TypedDict


class Point2D(TypedDict):
    x: int
    y: float


value1: Point2D = {"x": 1, "y": 1.5}  # OK
value2: Point2D = {"x": 1.5, "y": 1}  # NG

なお、この記述も他と同様インタプリタでは解釈されないです。

overload

Pythonではオーバーロードは基本的にされませんが、typing.overloadを利用することでmypyなどに向けて関数のオーバーライドが実装されているということを明示できます。例えば以下のようにすることで対応できます。

from typing import overload


@overload
def func(x: int) -> str:
    ...


@overload
def func(x: str) -> int:
    ...

def func(x):
    if isinstance(x, int):
        return str(x)
    elif isinstance(x, str):
        return len(x)
    else:
        raise TypeError("Unsupported type")

なお、上記のように具体的な実装は単一の関数であり、それ以外はあくまでオーバーロードの型だけ定義しています。

まとめ

今回はPythonのtypingについて、主要なよく目にするものについて紹介しました。公式ドキュメントではより詳細に記載されていますので、より深くキャッチアップされたい方はぜひドキュメントも参照してもらえたらと思います。型アノテーションはインタプリタには無視されるものの、開発する際のドキュメントとして機能する大変大事な要素なので、ぜひ開発する際は積極的にアノテーションをつけてもらえればと思います。

Discussion