🦊

PythonのTypedDictをネストする4つのパターン

2022/10/09に公開

PythonのTypedDictは辞書に対する型ヒントですが、ネスト構造の定義をする方法が不明確です。
本記事ではTypedDictをネストする方法とその解説を行います。

TypedDictについて

TypedDictは辞書型への型ヒントを提供するインターフェースです。
JSONやYAMLから生成した辞書に対して型ヒントを利用したいケースに便利です。

(※スクリプト実行時の型チェックをしたい場合はPyDanticを利用することをお勧めします。)

例えばこういったYAMLファイルで生成した辞書に対してTypedDictを定義するとエディタで型ヒントが出るようになるます。

サンプルYAMLファイル

version: '3.8'

services:
  example:
    image: python
    ports:
      - 10080:80
      - 18080:8080

型ヒント表示例(VSCode)


また、未定義のキーを参照した場合は警告を出してくれるのでこれまた便利です。

本題

そんな便利なTypedDictですが、これが提案されたPEP 589ではTypedDictをネストした際の挙動は定義されていません。
そのため、ネスト定義がうまくいったケースとうまくいかなかったケースをPythonの仕様を含めて紹介します。

個人的には本記事後述の型ヒントの前方参照を利用する方法を定義する方法をお勧めします。

失敗例

(失敗例) クラス内にクラスをネスト定義する

mypyやpyrightではTypedDictをクラス内クラスでネスト定義するとエラーとなります。

サンプルコード
※エラーが出るコードなのでコピペ防止に画像で

エラー出力例

pyrightでエラーが出る

mypyでエラーが出る

成功例

オーソドックスな方法

クラスのネスト定義がダメなので、TypedDictを個別に定義する。
※ただし、未定義の肩を参照するとエラーになるので定義する順番には注意が必要。

from typing import TypedDict
from typing import List

class _ContainerConfigType(TypedDict):
    image: str
    ports: List[str]

class _ServicesConfigType(TypedDict):
    example: _ContainerConfigType

class ConfigType(TypedDict):
    version: str
    services: _ServicesConfigType

注意事項

ただし↓みたいな定義方法はConfigTypeクラスで_ServicesConfigTypeが未定義のためエラーになります。

(コピペ防止のため画像サンプルコードは画像にて)

これを解決するために、次セクションで紹介する前方参照を使う方法があります。

(おすすめ) 型ヒントの前方参照を利用する方法

型ヒントの前方参照とは、未定義の型ヒントを文字列リテラルで指定することで、その行の後で定義された型を利用することです。

こちらの仕様はPEP 484で定義されています。
※前方参照はPythonのドキュメントでもあまり言及されておらず、Python 3.11で追加されたSelf型のサンプルコードに唯一説明が記載されているレベルです

from typing import List, TypedDict


class ConfigType(TypedDict):
    version: str
    services: "_ServicesConfigType"


class _ContainerConfigType:
    image: str
    ports: List[str]


class _ServicesConfigType(TypedDict):
    example: "_ContainerConfigType"

デメリット

  • 前方参照の仕様はあまり知られてないと思います。導入する際はチーム内で合意取りましょう。

さらに次セクションでは型ヒント用に前方参照を使わず未定義型を指定した場合でも評価される仕組みがPEP563で提案されています。

(非推薦) __future__インポートでPEP563を有効にする

PEP563は型ヒントの評価を遅延することで、未定義型を後から定義した型で参照するようにできる、前方参照とは別な仕組みです。

平たく言えば本記事のオーソドックスな方法でエラーが出るケースでエラーが出なくなる仕様です。

現在PEP563Pythonの正式機能ではないですが、Python 3.7からfrom __future__ import annotationsして利用できるようになります。

型ヒントの前方参照とPEP563の違いは、前者は型ヒントが文字列リテラルなのに対し後者は型を指定します。

比較用の図

注意事項

  • Pythonに正式導入されていない機能を使用しています、本番環境では絶対に使用しないでください
  • PEP563はPython3.10で正式導入される予定でしたがPyDanticの動作に影響がある等の事情で導入が延期され、Python 3.11時点ではどのバージョンで正式導入されるかの時期もTBDにされてしまいました。

サンプルコード

from __future__ import annotations

from typing import List, TypedDict


class ConfigType(TypedDict):
    version: str
    services: _ServicesConfigType


class _ContainerConfigType:
    image: str
    ports: List[str]


class _ServicesConfigType(TypedDict):
    example: _ContainerConfigType

(非推薦) クラス定義の代わりにTypedDict関数をネストして型を定義する

TypedDictはクラス内クラスの定義をすることはできませんが、TypedDict関数をネストして定義する場合は動作するようです。
こちらもPEP 589でネストして動作すると仕様で定義されている訳ではありません。そのためたまたま動作している可能性が高いです。

また、可読性が悪くなるので使用はおすすめしません。

from typing import List, TypedDict

ConfigType = TypedDict(
    "ConfigType",
    {
        "version": str,
        "services": TypedDict(
            "_ServicesConfigType",
            {
                "example": TypedDict(
                    "_ContainerConfigType", {"image": str, "ports": List[str]}
                )
            },
        ),
    },
)```

Discussion