`from __future__ import annotations` がPython 3.10でデフォルトにならなくなりました

PEP 563 は Python 3.10 でデフォルトになる予定で、実際に去年の10月から master ブランチでは有効になっていました。今までの Python 3.10 のアルファ版でも有効になっています。

www.python.org

このPEPはアノテーションの実行時の利用に後方非互換性と大幅な制限を加えてしまいます。

>>> class C:
...     a: int
...
>>> C.__annotations__
{'a': <class 'int'>}

この例では __annotations__ に int クラスのオブジェクトそのものが入っていますが、PEP 563が有効になると "int" という文字列が入るので、直接 __annotations__ を見ていたコードは動かなくなります。

PEP 563 が有効でなくてもPEP 484に従ったtype annotationでは、アノテーションが評価されるタイミングでは解決できない名前を a: "int" のように文字列で書くことでアノテーション利用時に評価していたので、それだけで動かなくなるコードはもともと不完全ともいえるのですが、 PEP 484 にはあまり関心がなく前方参照が無いアノテーションを前提にして書かれていたコードは動かなくなります。

また、名前解決のスコープの問題もあります。

>>> def f(t):
...     class C:
...         a: t
...     return C
...
>>> kls = f(str)
>>> kls.__annotations__
{'a': <class 'str'>}

この例では、PEP 563では kls.__annotations__{'a': 't'} になりますが、 typing.get_type_hints(kls) してもすでに t はモジュールのスコープにないので解決できません。

この例はもう完全にstatic typingが動かないのでPEP 563がAcceptされた時点では妥当な制限だったのですが、最近流行しているFastAPIが利用しているPydanticというライブラリがアノテーションを実行時の型情報を扱うために利用しており、実際にユーザーの中には上の例のようなコードを書いてしまっている人が居るようです。

PEP 563がAcceptされたのが2017年12月で、PydanticやFastAPIが盛り上がったのは完全にその後なのでこの状況は非常に残念なのですが、上のような利用法に対して適切にwarningを出す仕組みが存在しない(仕組み上出すのが非常に難しい)ので仕方ありません。

Pydanticは上のような利用法でも使えるようにする仕組み update_forward_refs() を提供しては居るのですが、その仕組みを明示的に呼び出していないユーザーのコードは壊れます。

Postponed annotations - pydantic

また、文字列化されたアノテーションPythonオブジェクトに変換するために eval() を使うのですがそれが遅いという問題もあります。

どの問題も致命的ではないものの、特にFastAPIのコミュニティーが移行の準備が全くできていないということで、PEP 563のデフォルト化は延期されることになりました。現在デフォルト化を取り消すプルリクエストがレビュー中です。

github.com

また、PEP 563の欠点のいくつかを改善するPEP 649も提案されています。アノテーションの今後については、Python 3.10が落ち着いてから慎重に議論されるでしょう。

なお、去年このBlogで紹介した PEP 563 向けの最適化はrevertされずにそのまま残ります。なので from __future__ import annotations を書いているユーザーは起動速度やメモリ使用量のオーバーヘッドが大幅に低減されます。

methane.hatenablog.jp

また単純な revert ではなく、この最適化を生かした形で from __future__ import annotations が書かれていない場合の実装が改めて書かれています。関数アノテーションfrom __future__ import annotations が書かれていなくても一旦タプルとして保存され、 func.__annotations__ にアクセスされた時にタプルからdictに変換されます。

多くはないですが、 def __init__(self) -> None: のようなアノテーションならPEP 563がなくても ('return', None) というタプルになり、これは定数 (int などは名前解決しないといけないので定数ではない) なので、コンパイル時にタプルが作られた上で、同じモジュールないの同じシグネチャでは同一のタプルオブジェクトを使い回せるという最適化も効いてきます。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。