typing-inspection
typing-inspection provides runtime tools to inspect Python type annotations at runtime. Maintained by the Pydantic team, it is split into two submodules: `typing_inspection.typing_objects` (predicate functions like `is_union`, `is_literal`, `is_any`, etc. that correctly handle both `typing` and `typing_extensions` variants) and `typing_inspection.introspection` (higher-level helpers such as `inspect_annotation`, `get_literal_values`, and `is_union_origin`). Current version is 0.4.2 (released 2025-10-01); the project has released frequently since its initial release in February 2025.
Warnings
- breaking The INFERRED sentinel was renamed to UNKNOWN in v0.3.0. Any code importing or comparing against INFERRED will get an ImportError or a broken identity check.
- breaking v0.4.0 added a new `DATACLASS` value to the AnnotationSource enum to support `dataclasses.InitVar` as a type qualifier. Exhaustive match/if-elif chains over AnnotationSource members will silently miss it.
- gotcha Never use identity checks like `get_origin(x) is typing.Union` or `get_origin(x) is typing_extensions.Union`; typing_extensions may ship a different Union object than stdlib typing.
- gotcha inspect_annotation() raises ForbiddenQualifier if a type qualifier is not allowed for the given AnnotationSource (e.g. using Required outside a TypedDict context). Not catching this causes unhandled exceptions at runtime.
- gotcha Using `type_expr.__args__` directly to get Literal values silently misses PEP 695 type aliases (Python 3.12+), which are lazily evaluated and may not be expanded.
- gotcha When `unpack_type_aliases='eager'` is passed to inspect_annotation(), any undefined symbol in a PEP 695 type alias raises NameError at runtime. The default is 'skip' (aliases not expanded).
- deprecated `is_union_origin()` is effectively superseded on Python 3.14+, where both Union[t1,t2] and t1|t2 produce the same typing.Union class. The function remains available but its main use case disappears.
Install
-
pip install typing-inspection
Imports
- inspect_annotation
from typing_inspection.introspection import inspect_annotation
- AnnotationSource
from typing_inspection.introspection import AnnotationSource
- UNKNOWN
from typing_inspection.introspection import UNKNOWN
- is_union
from typing_inspection.typing_objects import is_union
- get_literal_values
from typing_inspection.introspection import get_literal_values
- is_union_origin
from typing_inspection.introspection import is_union_origin
- ForbiddenQualifier
from typing_inspection.introspection import ForbiddenQualifier
Quickstart
from typing import ClassVar, Union, get_origin
from typing import Annotated
from typing_inspection.introspection import (
AnnotationSource,
UNKNOWN,
inspect_annotation,
get_literal_values,
is_union_origin,
)
from typing_inspection.typing_objects import is_any, is_literal, is_union
# --- Unwrap an annotation expression ---
result = inspect_annotation(
ClassVar[Annotated[int, "meta"]],
annotation_source=AnnotationSource.CLASS,
)
print(result)
# InspectedAnnotation(type=int, qualifiers={'class_var'}, metadata=['meta'])
# Check the UNKNOWN sentinel (bare ClassVar / Final with no inner type)
bare_result = inspect_annotation(ClassVar, annotation_source=AnnotationSource.CLASS)
if bare_result.type is UNKNOWN:
print("No explicit inner type; infer from assignment or default to Any")
# --- typing_objects predicates handle both typing + typing_extensions ---
from typing import Literal
origin = get_origin(Literal[1, 2])
print(is_literal(origin)) # True
# Safe union detection across typing / typing_extensions / PEP 604
union_origin = get_origin(Union[int, str])
print(is_union_origin(union_origin)) # True
# Retrieve Literal values (expands PEP 695 type aliases correctly)
values = get_literal_values(Literal["a", "b", 1])
print(values) # ('a', 'b', 1)