アノテーションのベストプラクティス

author:

Larry Hastings

オブジェクトのアノテーション辞書へのアクセス (Python 3.10 以降)

Python 3.10 で標準ライブラリに inspect.get_annotations() 関数が新たに追加されました。Python 3.10 から 3.13 までは、アノテーションに対応しているオブジェクトのアノテーション辞書にアクセスするには、この関数を使うのが一番良いです。文字列で指定されたアノテーションも自動的に文字列でない値に解決してくれます。

Python 3.14 では、アノテーションを扱うためのモジュール annotationlib が追加されました。このモジュールに annotationlib.get_annotations() があり、 inspect.get_annotations() を置き換えるものです。

もし何らかの理由で自分の用途に inspect.get_annotations() が合わないという場合は、手動で __annotations__ にアクセスすることもできます。これについても、推奨のアクセス方法が Python 3.10 で変わりました。3.10 で、 o.__annotations__ は関数、クラス、モジュールに対しては必ず使えることが保証されるようになりました。オブジェクトが確実にこの3つのいずれかである場合は、単に o.__annotations__ を使えばアノテーション辞書を取得できます。

しかし、それ以外の callable (例えば、 functools.partial() で作られたもの) は __annotations__ 属性が定義されていない可能性があります。この属性があるかどうか不明なオブジェクトの場合は、Python 3.10 以降では、 getattr() に3つの引数を渡して呼び出す (例えば getattr(o, '__annotations__', None) ) のを推奨します。

Python 3.10 より前のバージョンでは、親クラスにアノテーションがあるが自身はアノテーションを定義していないようなクラスの場合、 __annotations__ を取得すると親クラスの __annotations__ が返されます。3.10 以降では、代わりに空の辞書が返されます。

オブジェクトのアノテーション辞書へのアクセス (Python 3.9 以前)

Python 3.9 以前では、オブジェクトのアノテーション辞書を取得するのはより複雑です。原因は、3.9 以前のバージョンの設計上の欠陥 (具体的にはクラスのアノテーション) です。

クラス以外のオブジェクト (関数や他の callable、モジュール) のアノテーションを取得する推奨の方法は、3.10と同じです。 inspect.get_annotations() ではなく、 getattr() に3つの引数を渡すことで __annotations__ 属性を取得するのがいいです。

ただし、クラスについては推奨方法が異なります。なぜなら、クラスには __annotations__ は必須ではなく、属性はベースクラスから継承されるので、 __annotations__ 属性にアクセスするとベースクラスのアノテーション辞書を取得してしまうという問題があるからです。例えば:

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

このコードでは Derived ではなく Base のアノテーションが表示されます。

クラス (isinstance(o, type)) のアノテーションを取得するためには、新たに分岐を追加する必要があります。推奨する方法では、 Python 3.9 以前の実装に依存した方法をとります。クラスにアノテーションがあれば、 __dict__ の辞書に格納されるという実装になっています。クラスにはアノテーションがない場合もあるため、この辞書の get() メソッドを使ってアノテーションを取得する、というのが推奨の方法です。

以上を踏まえると、Python 3.9 以前では、以下のサンプルコードで任意のオブジェクトの __annotations__ 属性を安全に取得することができます:

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

このコードを実行すると、 ann の値は辞書か None となります。さらに ann の値を調べるには、 isinstance() で型を再度確認することを推奨します。

ただし、一部の特殊な型オブジェクトや不正な形式の型オブジェクトは __dict__ 属性がないこともあるため、さらに安全をとるには getattr() を使って __dict__ を取得したほうがいいです。

文字列指定のアノテーションを手動で解決する方法

アノテーションは文字列として指定されている場合があり、それを評価して Python の値に変換するには、 inspect.get_annotations() を使うのが一番です。

Python 3.9 以前を使っているか、もしくは何らかの理由で inspect.get_annotations() を使えない場合は、自分で実装する必要があります。現状の Python バージョンの inspect.get_annotations() の実装を確認して、同様の処理を実装することを推奨します。

すなわち、任意のオブジェクト o の文字列アノテーションを評価するには:

  • o がモジュールならば、 o.__dict__globals として eval() を呼ぶ。

  • o がクラスならば、 sys.modules[o.__module__].__dict__globals とし、 dict(vars(o))locals として eval() を呼ぶ。

  • ofunctools.update_wrapper()functools.wraps()functools.partial() でラップされた callable ならば、一番内部の関数に到達するまで、 o.__wrapped__o.func のどちらかを繰り返し取得する。

  • o がクラス以外の callable であれば、 o.__globals__ を globals として eval() を呼ぶ。

しかし、必ずしもアノテーションの文字列が eval() で Python の値に変換できるとは限りません。理論上どんな文字列も指定可能であり、変換できない文字列を型ヒントとして指定する必要がある場合が実際にあります。例えば:

  • | を使った PEP 604 のユニオン型 (これが追加された Python 3.10 より前のバージョン)

  • typing.TYPE_CHECKING が True の時だけインポートされ、実行時には必要ない型定義。

eval() で上記のような値を評価しようとすると、失敗し例外が発生します。そのため、アノテーションを扱うライブラリ API を作る際は、呼び出し側が明示的に要求したときのみ、文字列を評価することが推奨されます。

__annotations__ のベストプラクティス (全 Python バージョン共通)

  • オブジェクトの __annotations__ 属性に直接代入することは避けるべきです。Python に任せましょう。

  • 直接 __annotations__ 属性に代入するなら、必ず dict オブジェクトを代入するべきです。

  • どういうオブジェクトかに関わらず、直接 __annotations__ にアクセスするのは避けるべきです。Python 3.14 以降は annotationlib.get_annotations() 、 Python 3.10 以降は inspect.get_annotations() を使いましょう。

  • 直接 __annotations__ メンバーにアクセスするなら、まず値が辞書であることをチェックしてから中身を調べるべきです。

  • __annotations__ 辞書を書き換えるのは避けるべきです。

  • __annotations__ 属性を削除するのは避けるべきです。

__annotations__ の注意点

In all versions of Python 3, function objects lazy-create an annotations dict if no annotations are defined on that object. You can delete the __annotations__ attribute using del fn.__annotations__, but if you then access fn.__annotations__ the object will create a new empty dict that it will store and return as its annotations. Deleting the annotations on a function before it has lazily created its annotations dict will throw an AttributeError; using del fn.__annotations__ twice in a row is guaranteed to always throw an AttributeError.

Everything in the above paragraph also applies to class and module objects in Python 3.10 and newer.

In all versions of Python 3, you can set __annotations__ on a function object to None. However, subsequently accessing the annotations on that object using fn.__annotations__ will lazy-create an empty dictionary as per the first paragraph of this section. This is not true of modules and classes, in any Python version; those objects permit setting __annotations__ to any Python value, and will retain whatever value is set.

If Python stringizes your annotations for you (using from __future__ import annotations), and you specify a string as an annotation, the string will itself be quoted. In effect the annotation is quoted twice. For example:

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

This prints {'a': "'str'"}. This shouldn't really be considered a "quirk"; it's mentioned here simply because it might be surprising.

If you use a class with a custom metaclass and access __annotations__ on the class, you may observe unexpected behavior; see 749 for some examples. You can avoid these quirks by using annotationlib.get_annotations() on Python 3.14+ or inspect.get_annotations() on Python 3.10+. On earlier versions of Python, you can avoid these bugs by accessing the annotations from the class's __dict__ (for example, cls.__dict__.get('__annotations__', None)).

In some versions of Python, instances of classes may have an __annotations__ attribute. However, this is not supported functionality. If you need the annotations of an instance, you can use type() to access its class (for example, annotationlib.get_annotations(type(myinstance)) on Python 3.14+).