Python 3.9 introduces the Annotated
class which allows adding arbitrary metadata to type hints, e.g.,
class A:
x: Annotated[int, "this is x"]
The annotated type hint can be obtained by setting the new include_extras
argument of get_type_hints
:
>>> get_type_hints(A, include_extras=True)
{'x': typing.Annotated[int, 'this is x']}
And the metadata itself can be accessed through the __metadata__
attribute of the type hint.
>>> h = get_type_hints(A, include_extras=True)
>>> h["x"].__metadata__
('this is x',)
But, my question is, what is the right way to test if a type hint even is Annotated
? That is, something akin to:
if IS_ANNOTATED(h["x"]):
# do something with the metadata
As far as I can tell, there is no documented method to do so, and there are a couple possible ways, none of which seem ideal.
Comparing the type
to Annotated
doesn't work because the type hint is not an instance of Annotated
:
>>> type(h["x"])
typing._AnnotatedAlias
So we have to do:
if type(h["x"]) is _AnnotatedAlias:
...
But, given the leading underscore in _AnnotatedAlias
, this requires using, presumably, an implementation detail.
The other option is to directly check for the __metadata__
attribute:
if hasattr(h["x"], "__metadata__"):
...
But this assumes that the __metadata__
attribute is unique to Annotated
, which can't necessarily be assumed when dealing with user-defined type hints too.
So, is there perhaps a better way to do this test?
How about this?
from typing import Annotated, Any
annot_type = type(Annotated[int, 'spam'])
def is_annotated(hint: Any, annot_type=annot_type) -> bool:
return (type(hint) is annot_type) and hasattr(hint, '__metadata__')
Or, using the newish PEP 647:
from typing import Annotated, TypeGuard, Any
annot_type = type(Annotated[int, 'spam'])
def is_annotated(hint: Any, annot_type=annot_type) -> TypeGuard[annot_type]:
return (type(hint) is annot_type) and hasattr(hint, '__metadata__')
This solution sidesteps having to use any implementation details directly. I included the extra hasattr(hint, '__metadata__')
test in there just for safety.
Interestingly, this solution seems to be pretty similar to the way Python currently implements several functions in the inspect
module. The current implementation of inspect.isfunction
is as follows:
# inspect.py
# -- snip --
import types
# -- snip --
def isfunction(object):
"""Return true if the object is a user-defined function.
Function objects provide these attributes:
__doc__ documentation string
__name__ name with which this function was defined
__code__ code object containing compiled function bytecode
__defaults__ tuple of any default values for arguments
__globals__ global namespace in which this function was defined
__annotations__ dict of parameter annotations
__kwdefaults__ dict of keyword only parameters with defaults"""
return isinstance(object, types.FunctionType)
So then you go to the types
module to find out the definition of FunctionType
, and you find it is defined like so:
# types.py
"""
Define names for built-in types that aren't directly accessible as a builtin.
"""
# -- snip --
def _f(): pass
FunctionType = type(_f)
Because, of course, the exact nature of a function
object is dependent on implementation details of Python at the C level.