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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With