Diary

Diary

日々学んだことをアウトプットする場として初めてみました

久々に Python を触ったら型周りが進化してた

すごい久々にちゃんと Python の知識を update しようとしてるのですが、見える世界が全く変わっていたのでその感動をメモしておきます。
(当時から自分の知識が増えたことも大きいです。)

以下 mypy などの型チェッカーと使うことを前提にしています。

$ python --version
Python 3.13.1

[目次]

  • typing.NamedTuple
    • typing.TypedDict との違い
  • Literal の Union
  • typing.Protocol
  • typing.ReadOnly
  • おわりに

typing.NamedTuple

typing.NamedTuple は Python v3.6 で追加されており、名前付きのタプルを生成できます。

型ヒントにより VSCode 等で補完が効くところが collections.namedtuple との違いです。

from typing import NamedTuple

class CompanyInfo(NamedTuple):
    name: str
    # default 値をとれる。
    phone: str | None = None

# c = CompanyInfo("sample", "123-4567") とするより好き。
c = CompanyInfo(name="sample", phone="123-4567")
print('c.name: ', c.name)

# tuple ぽさも当然あるが、意図しない挙動につながるため個人的には使いたくない。
# index access.
print('c[0]: ', c[0])
# unpacking.
name, phone = c
print('name: ', name)
# init with tuple.
c2 = CompanyInfo(*c)
print('c2: ', c2)

結局は Tuple であるため、インデックスアクセス・アンパッキングができることには注意が必要です。

typing.TypedDict との違い

TypedDict は辞書由来の型ヒントを提供する NamedTuple のようなもので、その違いは辞書とタプルの違いがそのまま反映されてます。

フィールドへのアクセス方法の違い、ミュータブル性の違いなどがあります。

from typing import TypedDict

class CompanyInfoTypedDict(TypedDict):
    name: str
    # default 値をとれる。
    phone: str | None = None

typed_data = CompanyInfoTypedDict(name="Alice", phone="123-4567")
# TypedDict は辞書なので基本的には mutable.
typed_data['name'] = "Bob"
# typed_data:  {'name': 'Bob', 'phone': '123-4567'}
print('typed_data: ', typed_data)

特に希望がない場合は immutable な NamedTuple を使えば良さそうです。

Literal の Union

TypeScript を使った時に Literal の Union に非常に感動したのですが、Python でも 3.8 から typing.Literal が使えるようになりました。

from typing import Literal

# 配列で渡すと Union になる。
type JobStatus = Literal["running", "stopped", "pending"]

class Job:
    def __init__(self):
        self.status = "running"

以下のように typo してしまった場合、mypy が教えてくれます。

...
class Job:
    def __init__(self) -> None:
        # running とすべきところを runningw としてしまった。
        self.status: JobStatus = "runningw"

job = Job()
print(job.status)
$ uv run mypy typecheck.py
typecheck.py:8: error: Incompatible types in assignment (expression has type "Literal['runningw']", variable has type "Literal['running', 'stopped', 'pending']")  [assignment]

また Union と明示したり TypeScript のように | でも記述可能です。

# 同じ意味。
JobStatus = (
    Literal["running"]
    | Literal["stopped"]
    | Literal["pending"]
)
JobStatus = Union[
    Literal["running"],
    Literal["stopped"],
    Literal["pending"],
]

typing.Protocol

振る舞いを元にインターフェースを定義するためのもので、Python 3.8 で追加されています。

from typing import Protocol

class Animal(Protocol):
    def speak(self) -> None:
        pass

class Dog:
    def speak(self) -> None:
        print("Woof!")

def speak(animal: Animal) -> None:
    animal.speak()

dog = Dog()
speak(dog)

Protocol で定義したメソッドを満たさない場合は、mypy が教えてくれます。

class Foo:
    def bar(self) -> None:
        print("Bar!")

f = Foo()
speak(f)
$ uv run mypy .
proto.py:22: error: Argument 1 to "speak" has incompatible type "Foo"; expected "Animal"  [arg-type]

typing.ReadOnly

読み取り専用の型ヒントを提供するもので、Python 3.13 で追加されてます。

先述の TypedDict と組み合わせることで、一部のフィールドのみ可変にできます。

from typing import ReadOnly, TypedDict

class CompanyInfoTypedDict(TypedDict):
    name: ReadOnly[str]
    # 特定の値のみ可変にする。
    phone: ReadOnly[str | None] = None

おわりに

結局 typing モジュールの中で自分が興味を持ったものの紹介になってしまいました。

typing はマイバージョン進化してそうなので、一通り眺めてみるのも良さそうです!

他にもおすすめの機能があれば教えてください!