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

author:

  Larry Hastings

概要

このドキュメントは、アノテーションの辞書を扱う上でのベストプラクティスをまとめるものです。Python オブジェクトに付与された `__annotations__` を Python コードからアクセスする際には、以下のガイドラインに従うことを推奨します。

本ドキュメントは次の４セクションで構成されています。Python 3.10 以降でオブジェクトのアノテーションにアクセスするときのベストプラクティス、Python 3.9 以下でオブジェクトのアノテーションにアクセスするときのベストプラクティス、Python バージョンに関わらない `__annotations__` に関するベストプラクティス、 `__annotations__` の注意点、です。

注意：本ドキュメントは、アノテーションの使い方ではなく、 `__annotations__` の使い方を扱っています。型ヒントをコードの中でどうやって使うかについては、 `typing` モジュールを参照してください。

## オブジェクトのアノテーション辞書へのアクセス (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__` は関数、クラス、モジュールに対しては必ず使えることが保証されるようになりました。オブジェクトが確実にこの３つのいずれかである場合は、単に `o.__annotations__` を使えばアノテーション辞書を取得できます。

しかし、それ以外の callable (例えば、 `functools.partial()` で作られたもの) は `__annotations__` 属性が定義されていない可能性があります。この属性があるかどうか不明なオブジェクトの場合は、Python 3.10 以降では、 `getattr()` に３つの引数を渡して呼び出す (例えば `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()` に３つの引数を渡すことで `__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()` を呼ぶ。

- `o` が `functools.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+).