Usage¶
The library is divided into two submodules:
-
typing_inspection.typing_objects
: provides functions to check if a variable is atyping
object:from typing_extensions import Union, get_origin from typing_inspection.typing_objects import is_union is_union(get_origin(Union[int, str])) # True
Note
You might be tempted to use a simple identity check:
>>> get_origin(Union[int, str]) is typing.Union
However,
typing_extensions
might provide a different version of thetyping
objects. Instead, thetyping_objects
functions make sure to check against both variants, if they are different. -
typing_inspection.introspection
: provides high-level introspection functions, taking runtime edge cases into account.
Inspecting annotations¶
If, as a library, you rely heavily on type hints, you may encounter subtle unexpected behaviors and performance issues when inspecting annotations. As such, this section provides a recommended workflow to do so.
Fetching type hints¶
The first step is to gather the type annotations from the object you want to inspect. The
typing.get_type_hints()
function can be used to do so. If you want to make use of annotated
metadata, make sure to set the include_extras
argument to True
.
>>> class A:
... x: int
... y: Annotated[int, ...]
...
>>> get_type_hints(A, include_extras=True)
{'x': int, 'y': Annotated[int, ...]}
Note
Currently, typing-inspection
does not provide any utility to fetch (and evaluate) type annotations. The current
typing
utilities might contain subtle bugs across the different Python versions, so there is value in
having similar functionality. It might be best to wait for PEP 649 to be fully
implemented first. In the meanwhile, the typing_extensions.get_type_hints()
backport can be used.
Unpacking metadata and qualifiers¶
The annotations fetched in the previous step are called annotation expressions.
An annotation expression is a type expression, optionally surrounded by one or more type qualifiers
or by the Annotated
form.
For instance, in the following example:
from typing import Annotated, ClassVar
class A:
x: ClassVar[Annotated[int, "meta"]]
The type hint of x
is an annotation expression. The underlying type expression is int
. It is wrapped
by the ClassVar
type qualifier, and the Annotated
special form.
The goal of this step is to:
- Unwrap the underlying type expression.
- Keep track of the type qualifiers and annotated metadata.
To unwrap the type hint, use the inspect_annotation()
function:
>>> from typing_inspection.introspection import AnnotationSource, inspect_annotation
>>> inspect_annotation(
... ClassVar[Annotated[int, "meta"]],
... annotation_source=AnnotationSource.CLASS,
... )
...
InspectedAnnotation(type=int, qualifiers={"class_var"}, metadata=["meta"])
Note that depending on the annotation source, different type qualifiers can be (dis)allowed.
For instance, TypedDict
classes allow Required
and NotRequired
,
which are not allowed elsewhere (the allowed typed qualifiers are documented in the
AnnotationSource
enum class).
A ForbiddenQualifier exception is raised if an invalid qualifier is used.
If you want to allow all of them, use the AnnotationSource.ANY
annotation
source.
The result of the inspect_annotation()
function contains the underlying
type expression, the qualifiers and the annotated metadata. Note that some qualifiers are allowed to be used without any
type expression. In this case, the type should be inferred from the assigned value:
class A:
x: Annotated[Final, 'meta'] = 1 # type of x should be inferred to `int`.
In this case, the InspectedAnnotation.type
attribute is set
to the INFERRED
sentinel value, so you should check for this sentinel value
before doing any processing on the type:
from typing_inspection.introspection import INFERRED, AnnotationSource, inspect_annotation
inspected_annotation = inspect_annotation(
Annotated[Final, 'meta'],
annotation_source=AnnotationSource.CLASS,
)
if inspected_annotation.type is INFERRED:
ann_type = type(assigned_value) # assigned_value would come from the class
else:
ann_type = inspected_annotation.type
Parsing PEP 695 type aliases
In Python 3.12, the new type statement can be used to define type aliases.
When a type alias is wrapped by the Annotated
form, the type alias' value will not be unpacked by Python
at runtime. This means that while the following is technically valid:
type MyInt = Annotated[int, "int_meta"]
class A:
x: Annotated[MyInt, "other_meta"]
it might be necessary to parse the type alias during annotation inspection. This behavior can be controlled using the
unpack_type_aliases
parameter:
>>> inspect_annotation(
... Annotated[MyInt, "other_meta"],
... annotation_source=AnnotationSource.CLASS,
... unpack_type_aliases="eager",
... )
...
InspectedAnnotation(type=int, qualifiers={}, metadata=["int_meta", "other_meta"])
Whether you should unpack type aliases depends on your use case. If the annotated metadata present in the type alias is only meant to be applied on the annotated type (and not the attribute that will be type hinted), you probably need to keep type aliases as is, and possibly error later if invalid metadata is found when inspecting the type alias.
Note that type aliases are lazily evaluated. During type alias inspection, any undefined symbol
will raise a NameError
. To prevent this from happening, you can use 'skip'
to avoid expanding
type aliases (the default), or 'lenient'
to fallback to 'skip'
if the type alias contains an undefined
symbol:
>>> type BrokenType = Annotated[Undefined, ...]
>>> type MyAlias = Annotated[BrokenType, "meta"]
>>> inspect_annotation(
... MyAlias,
... annotation_source=AnnotationSource.CLASS,
... unpack_type_aliases="lenient",
... )
...
InspectedAnnotation(type=BrokenType, qualifiers={}, metadata=["meta"])
Inspecting the type expression¶
With the qualifiers and Annotated
forms removed, we can now proceed to inspect
the type expression.
First of all, some simple typing special forms can be checked:
from typing_inspection.typing_objects import is_any, is_self
# This would come from `InspectedAnnotation.type`, after checking for `INFERRED`:
type_expr = ...
if is_any(type_expr):
... # Handle `typing.Any`
if is_self(type_expr):
... # Handle `typing.Self`
We will then use the typing.get_origin()
function to fetch the origin of the type. Depending
on the type, the origin has different meanings:
from typing_inspection.introspection import get_literal_values, is_union_origin
from typing_inspection.typing_objects import is_annotated, is_literal
origin = get_origin(type_expr)
if is_union_origin(origin):
# Handle `typing.Union` (or the new `|` syntax)
union_args = type_expr.__args__
...
# You may also want to check for Annotated forms. While we unwrapped them
# in step 2, `Annotated` can be used in parts of the annotation, e.g.
# `list[Annotated[int, ...]]`:
if is_annotated(origin):
annotated_type = type_expr.__origin__ # not to be confused with the origin above
metadata = type_expr.__metadata__
if is_literal(origin):
# Handle `typing.Literal`
literal_values = get_literal_values(type_expr)
While Literal
values can be retrieved using type_expr.__args__
, the
get_literal_values()
function ensures
PEP 695 type aliases are properly expanded.
Next, we will take care of the typing aliases deprecated by PEP 585.
For instance, typing.List
is deprecated and replaced by the built-in list
type. In this case,
the origin of an unparameterized deprecated type alias is the replacement type, so we will use this one:
from typing_inspection.typing_objects import DEPRECATED_ALIASES
# If `type_expr` is `typing.List`, `origin` is the built-in `list`.
# We thus replace `type_expr` with `list`, and set `origin` to `None`
# to emulate the same behavior if `type_expr` was `list` in the beginning:
if origin is not None and type_expr in DEPRECATED_ALIASES:
type_expr = origin
origin = None
At this point, if origin
is not None
, you can safely assume that type_expr
is a parameterized generic type.
You can then define your own logic to handle the type expression, and have different code paths if you are
dealing with a parameterized type (e.g. list[int]
) or a "bare" type:
if origin is not None:
handle_generic_type(type=origin, arguments=type_expr.__args__)
else:
handle_type(type=type_expr)
Note
If a deprecated type alias is parameterized (e.g. typing.List[int]
), the origin will be the
replacement type (e.g. list
), and not the deprecated alias (e.g. typing.List
). This means
that handling typing.List[int]
or list
should be equivalent.