はじめに
従来、Pythonの型アノテーションは「型を定義するより前にアノテーションで使うと実行時にNameError
が送出される」仕様でした。
Python3.14ではこれが解消され、定義順によってエラーが発生することはなくなります。
この変更により文字列アノテーション、ForwardRef
やTYPE_CHECKING
といった特別なテクニックを使わずに、型アノテーションを書くことができるようになります。
def bar(foo: Foo): ... # Python 3.13以前ではNameErrorが発生するためこの書き方ができない。
# そのため、
#
# def bar(foo: "Foo"): ...
#
# または、
# def bar(foo: ForwardRef["Foo"]): ...
#
# または、
# if TYPE_CHECKING:
# def bar(foo: Foo): ... # 型ヒント用の関数定義
# else:
# def bar(foo):
# # 実行時に呼び出される実装
# ...
#
# にする必要がある
class Foo: ... # 型アノテーションで使われるより後の行で型が定義されている
私はこの挙動が実現したのは、前方参照してもエラーを発生させなくするfrom __future__ import annotations
がデフォルト動作になったことによるものだと思っていました。
しかし、オブジェクトが保持している情報を直接取得したり、PEPやドキュメントを読み進めるうちに、annotations
をインポートした状態がデフォルトになったのではないことがわかってきました。
この記事では、Python 3.14で導入された新しいアノテーションの評価方法について詳しく解説します。
Python 3.13とPython 3.14の挙動の違い
__future__
インポートを使用しない場合と使用する場合を比較してみます。
def bar(foo: Foo): ...
class Foo: ...
print(bar.__annotations__)
from __future__ import annotations
def bar(foo: Foo): ...
class Foo: ...
print(bar.__annotations__)
Python 3.13の挙動
Python 3.13環境で__future__
なしのコードを実行すると、Foo
がまだ定義されていないためNameError
が発生します。
$ python sample.py
Traceback (most recent call last):
File ".../sample.py", line 1, in <module>
def bar(foo: Foo): ...
^^^
NameError: name 'Foo' is not defined
Python 3.13環境で__future__
ありのコードを実行すると、__future__
インポートによってアノテーションが文字列として評価されるため、エラーは発生しません。
$ python sample.py
{'foo': 'Foo'}
また、関数の__annotations__
属性からは{引数名: 型名文字列}
の辞書が取得できます。
Python 3.14の挙動
では、Python 3.14環境ではどうなるでしょうか。
$ python sample.py
{'foo': 'Foo'}
__future__
ありのときはPython 3.13と同じく関数の__annotations__
属性から{引数名: 型名文字列}
の辞書が取得できます。
$ python sample.py
{'foo': <class '__main__.Foo'>}
__future__
なしのときはPython 3.13とは違いエラーにならず、{引数名: クラス}
となります。
これは、from __future__ import annotations
を行えばアノテーションされた型を文字列として評価するとしたPEP563の仕様とは異なります。
Python 3.14からはPEP563のデフォルト化とは別の仕様となったことが分かったので、調査してみました。
仕様変更についてのPEP
調査したところ、下記のPEPによってPython 3.14からのアノテーション評価の仕様が定められたことがわかりました。
PEP 649 - Deferred Evaluation Of Annotations Using Descriptors
これは アノテーションの遅延評価 (deferred evaluation) を導入するPEPです。
PEP563は前方参照問題を解決する手段として導入されましたが、実行時にアノテーション情報を利用するpydantic
やfastapi
のようなライブラリとの相性が悪い状態でした。
包括的に問題を解決するため、アノテーションがアクセスされるまで評価を遅らせることがこのPEPで提案されました。
これにより、型ヒントを記述する際に、定義順序を気にする必要がなくなります。
そして、実行時の型アノテーション情報を取得する際は型名文字列ではなく、クラスそのものを取得するようになりました。
PEP 749 – Implementing PEP 649
これはPEP649のアノテーション遅延評価モデルの実装に伴い、様々な既存機能の変更や新規標準モジュール(annotationlib
)の追加を行うPEPです。
その一つとして、from __future__ import annotations
は将来的に非推奨化のち廃止されます。これにより、__future__
インポートの必要がなくなり、コードのシンプル化が図られます。
変化の概要
Pythonの型アノテーション評価方法がどのように変化していったかをまとめると、下記のようになります。
-
Stock semantics:
Python 3.13までのデフォルトの評価方法です。
アノテーションはソースコード中に現れた時点で即座に評価("eagerly evaluated")されます。これにより、アノテーション内でまだ定義されていない名前を参照すると実行時にNameError
が発生します。 -
Stringized annotations:
Python 3.7以降、from __future__ import annotations
を使用した際に適用できる評価方法です。
アノテーションが文字列として評価されます。これにより、前方参照の問題は解決しましたが、実行時にアノテーション情報を取得する際に処理が複雑になるという新たな問題が生じました。 -
Deferred evaluation:
Python 3.14以降のデフォルトとなる評価方法です。
アノテーションは、アクセスされたときに初めて遅延的に評価("lazily evaluated")されます。前方参照の問題を解決しつつ、実行時にアノテーション情報が取得しやすくなることを両立します。
まとめ
Python 3.14で導入された遅延評価は、型アノテーションの体験を大きく向上させるものです。
これまでクラスの定義順によっては特殊なテクニックが必要だった型ヒントは、より直感的で使いやすいものへと進化しています。
「型ヒントのために特別なことをしなくてもいいようにする」という点では、PEP695で型変数を定義しやすくなったことと同一方向である言語機能の改善だと考えています。
アノテーションの遅延評価に関する変更は、ユーザーの利便性を考えるPythonコミュニティの姿勢を反映した重要な一歩であり、動的型付けと静的型チェックの利点を高い次元で両立しようとする、Pythonの継続的な進化を示すものだと考えています。