pythontype-hintingpython-typing

What is the right way to check if a type hint is Annotated?


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?


Solution

  • 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.

    Discussion of this solution

    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.