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

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()" を呼ぶ
  。

* If "o" is a wrapped callable using "functools.update_wrapper()",
  "@functools.wraps", or "functools.partial()", iteratively unwrap it
  by accessing either "o.__wrapped__" or "o.func" as appropriate,
  until you have found the root unwrapped function.

* "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+).
