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

Question:

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?

Asked By: Jayanth Koushik

||

Answers:

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.

Answered By: Alex Waygood

You can do this with typing.get_origin (https://docs.python.org/3/library/typing.html#typing.get_origin):

assert typing.get_origin(Annotated[int, 0]) is Annotated

assert typing.get_args(Annotated[int, 0]) == (int, 0)
Answered By: Conchylicultor