Deferred annotations make using operators potentially problematic at runtime.
If a value is not yet defined it’s not possible to know what the operator should do. Annotations don’t necessarily have to be types so annotationlib refuses to guess and defers the whole expression. This makes it impossible to get the other details out of an annotation if any of the names are undefined.
Using something like Annotated on the other hand, makes the intention clear and you can retrieve the other information even if the type is not yet defined (on Main - there was a bug that prevented this and the backport of the fix to 3.14 hasn’t been merged yet).
from annotationlib import get_annotations, Format
from typing import Annotated
from typing_extensions import Doc
from pprint import pp
class Example:
a: undefined @ Doc("useful info")
b: Annotated[undefined, Doc("useful info")]
annotations = get_annotations(Example, format=Format.FORWARDREF)
pp(annotations)
{'a': ForwardRef('undefined @ __annotationlib_name_1__', is_class=True, owner=<class '__main__.Example'>),
'b': typing.Annotated[ForwardRef('undefined', is_class=True, owner=<class '__main__.Example'>), Doc('useful info')]}
Here, even if the type is not yet defined it is possible to extract the other information from Annotated, but not from the syntax.
You might argue that this is the case only because @ hasn’t been defined as meaning Annotated yet, but actually this issue actually already exists for unions with the | syntax under 3.14.
from annotationlib import get_annotations, Format
from typing import Union
from pprint import pp
class Example:
a: str | undefined
b: Union[str, undefined]
pp(get_annotations(Example, format=Format.FORWARDREF))
{'a': ForwardRef('__annotationlib_name_1__ | undefined', is_class=True, owner=<class '__main__.Example'>),
'b': str | ForwardRef('undefined', is_class=True, owner=<class '__main__.Example'>)}
Here you can find that str is a valid type for ‘b’ at runtime, but you can’t extract that information for ‘a’.