Coverage for src/typing_inspection/typing_objects.py: 94%
122 statements
« prev ^ index » next coverage.py v7.8.1, created at 2026-07-30 07:22 +0000
« prev ^ index » next coverage.py v7.8.1, created at 2026-07-30 07:22 +0000
1"""Low-level introspection utilities for [`typing`][] members.
3The provided functions in this module check against both the [`typing`][] and [`typing_extensions`][]
4variants, if they exists and are different.
5"""
6# ruff: noqa: UP006
8import collections.abc
9import contextlib
10import re
11import sys
12import typing
13import warnings
14from textwrap import dedent
15from types import FunctionType, GenericAlias, NoneType
16from typing import Any, Final
18import typing_extensions
19from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated
21__all__ = (
22 'DEPRECATED_ALIASES',
23 'NoneType',
24 'is_annotated',
25 'is_any',
26 'is_classvar',
27 'is_concatenate',
28 'is_deprecated',
29 'is_final',
30 'is_forwardref',
31 'is_generic',
32 'is_literal',
33 'is_literalstring',
34 'is_namedtuple',
35 'is_never',
36 'is_newtype',
37 'is_nodefault',
38 'is_noextraitems',
39 'is_noreturn',
40 'is_notrequired',
41 'is_paramspec',
42 'is_paramspecargs',
43 'is_paramspeckwargs',
44 'is_readonly',
45 'is_required',
46 'is_self',
47 'is_typealias',
48 'is_typealiastype',
49 'is_typeguard',
50 'is_typeis',
51 'is_typevar',
52 'is_typevartuple',
53 'is_union',
54 'is_unpack',
55)
57_IS_PY310 = sys.version_info[:2] == (3, 10)
60def _compile_identity_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType:
61 """Create a function checking that the function argument is the (unparameterized) typing `member`.
63 The function will make sure to check against both the `typing` and `typing_extensions`
64 variants as depending on the Python version, the `typing_extensions` variant might be different.
65 For instance, on Python 3.9:
67 ```pycon
68 >>> from typing import Literal as t_Literal
69 >>> from typing_extensions import Literal as te_Literal, get_origin
71 >>> t_Literal is te_Literal
72 False
73 >>> get_origin(t_Literal[1])
74 typing.Literal
75 >>> get_origin(te_Literal[1])
76 typing_extensions.Literal
77 ```
78 """
79 in_typing = hasattr(typing, member)
80 in_typing_extensions = hasattr(typing_extensions, member)
82 if in_typing and in_typing_extensions:
83 if getattr(typing, member) is getattr(typing_extensions, member):
84 check_code = f'obj is typing.{member}'
85 else:
86 check_code = f'obj is typing.{member} or obj is typing_extensions.{member}'
87 elif in_typing and not in_typing_extensions: 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 check_code = f'obj is typing.{member}'
89 elif not in_typing and in_typing_extensions: 89 ↛ 92line 89 didn't jump to line 92 because the condition on line 89 was always true
90 check_code = f'obj is typing_extensions.{member}'
91 else:
92 check_code = 'False'
94 func_code = dedent(f"""
95 def {function_name}(obj: Any, /) -> bool:
96 return {check_code}
97 """)
99 locals_: dict[str, Any] = {}
100 globals_: dict[str, Any] = {'Any': Any, 'typing': typing, 'typing_extensions': typing_extensions}
101 exec(func_code, globals_, locals_)
102 return locals_[function_name]
105def _compile_isinstance_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType:
106 """Create a function checking that the function is an instance of the typing `member`.
108 The function will make sure to check against both the `typing` and `typing_extensions`
109 variants as depending on the Python version, the `typing_extensions` variant might be different.
110 """
111 in_typing = hasattr(typing, member)
112 in_typing_extensions = hasattr(typing_extensions, member)
114 if in_typing and in_typing_extensions:
115 if getattr(typing, member) is getattr(typing_extensions, member):
116 check_code = f'isinstance(obj, typing.{member})'
117 else:
118 check_code = f'isinstance(obj, (typing.{member}, typing_extensions.{member}))'
119 elif in_typing and not in_typing_extensions: 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true
120 check_code = f'isinstance(obj, typing.{member})'
121 elif not in_typing and in_typing_extensions: 121 ↛ 124line 121 didn't jump to line 124 because the condition on line 121 was always true
122 check_code = f'isinstance(obj, typing_extensions.{member})'
123 else:
124 check_code = 'False'
126 func_code = dedent(f"""
127 def {function_name}(obj: Any, /) -> 'TypeIs[{member}]':
128 return {check_code}
129 """)
131 locals_: dict[str, Any] = {}
132 globals_: dict[str, Any] = {'Any': Any, 'typing': typing, 'typing_extensions': typing_extensions}
133 exec(func_code, globals_, locals_)
134 return locals_[function_name]
137# Keep this ordered, as per `typing.__all__`:
139is_annotated = _compile_identity_check_function('Annotated', 'is_annotated')
140is_annotated.__doc__ = """
141Return whether the argument is the [`Annotated`][typing.Annotated] [special form][].
143```pycon
144>>> is_annotated(Annotated)
145True
146>>> is_annotated(Annotated[int, ...])
147False
148```
149"""
151is_any = _compile_identity_check_function('Any', 'is_any')
152is_any.__doc__ = """
153Return whether the argument is the [`Any`][typing.Any] [special form][].
155```pycon
156>>> is_any(Any)
157True
158```
159"""
161is_classvar = _compile_identity_check_function('ClassVar', 'is_classvar')
162is_classvar.__doc__ = """
163Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][].
165```pycon
166>>> is_classvar(ClassVar)
167True
168>>> is_classvar(ClassVar[int])
169>>> False
170```
171"""
173is_concatenate = _compile_identity_check_function('Concatenate', 'is_concatenate')
174is_concatenate.__doc__ = """
175Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][].
177```pycon
178>>> is_concatenate(Concatenate)
179True
180>>> is_concatenate(Concatenate[int, P])
181False
182```
183"""
185is_final = _compile_identity_check_function('Final', 'is_final')
186is_final.__doc__ = """
187Return whether the argument is the [`Final`][typing.Final] [type qualifier][].
189```pycon
190>>> is_final(Final)
191True
192>>> is_final(Final[int])
193False
194```
195"""
198# Unlikely to have a different version in `typing-extensions`, but keep it consistent.
199# Also note that starting in 3.14, this is an alias to `annotationlib.ForwardRef`, but
200# accessing it from `typing` doesn't seem to be deprecated.
201is_forwardref = _compile_isinstance_check_function('ForwardRef', 'is_forwardref')
202is_forwardref.__doc__ = """
203Return whether the argument is an instance of [`ForwardRef`][typing.ForwardRef].
205```pycon
206>>> is_forwardref(ForwardRef('T'))
207True
208```
209"""
212is_generic = _compile_identity_check_function('Generic', 'is_generic')
213is_generic.__doc__ = """
214Return whether the argument is the [`Generic`][typing.Generic] [special form][].
216```pycon
217>>> is_generic(Generic)
218True
219>>> is_generic(Generic[T])
220False
221```
222"""
224is_literal = _compile_identity_check_function('Literal', 'is_literal')
225is_literal.__doc__ = """
226Return whether the argument is the [`Literal`][typing.Literal] [special form][].
228```pycon
229>>> is_literal(Literal)
230True
231>>> is_literal(Literal["a"])
232False
233```
234"""
237# `get_origin(Optional[int]) is Union`, so `is_optional()` isn't implemented.
239is_paramspec = _compile_isinstance_check_function('ParamSpec', 'is_paramspec')
240is_paramspec.__doc__ = """
241Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec].
243```pycon
244>>> P = ParamSpec('P')
245>>> is_paramspec(P)
246True
247```
248"""
250# Protocol?
252is_typevar = _compile_isinstance_check_function('TypeVar', 'is_typevar')
253is_typevar.__doc__ = """
254Return whether the argument is an instance of [`TypeVar`][typing.TypeVar].
256```pycon
257>>> T = TypeVar('T')
258>>> is_typevar(T)
259True
260```
261"""
263is_typevartuple = _compile_isinstance_check_function('TypeVarTuple', 'is_typevartuple')
264is_typevartuple.__doc__ = """
265Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple].
267```pycon
268>>> Ts = TypeVarTuple('Ts')
269>>> is_typevartuple(Ts)
270True
271```
272"""
274is_union = _compile_identity_check_function('Union', 'is_union')
275is_union.__doc__ = """
276Return whether the argument is the [`Union`][typing.Union] [special form][].
278This function can also be used to check for the [`Optional`][typing.Optional] [special form][],
279as at runtime, `Optional[int]` is equivalent to `Union[int, None]`.
281```pycon
282>>> is_union(Union)
283True
284>>> is_union(Union[int, str])
285False
286```
288!!! warning
289 This does not check for unions using the [new syntax][types-union] (e.g. `int | str`).
290"""
293def is_namedtuple(obj: Any, /) -> bool:
294 """Return whether the argument is a named tuple type.
296 This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the
297 [`collections.namedtuple`][] factory function.
299 ```pycon
300 >>> class User(NamedTuple):
301 ... name: str
302 ...
303 >>> is_namedtuple(User)
304 True
305 >>> City = collections.namedtuple('City', [])
306 >>> is_namedtuple(City)
307 True
308 >>> is_namedtuple(NamedTuple)
309 False
310 ```
311 """
312 return isinstance(obj, type) and issubclass(obj, tuple) and hasattr(obj, '_fields') # pyright: ignore[reportUnknownArgumentType]
315# TypedDict?
317# BinaryIO? IO? TextIO?
319is_literalstring = _compile_identity_check_function('LiteralString', 'is_literalstring')
320is_literalstring.__doc__ = """
321Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][].
323```pycon
324>>> is_literalstring(LiteralString)
325True
326```
327"""
329is_never = _compile_identity_check_function('Never', 'is_never')
330is_never.__doc__ = """
331Return whether the argument is the [`Never`][typing.Never] [special form][].
333```pycon
334>>> is_never(Never)
335True
336```
337"""
339is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype')
340is_newtype.__doc__ = """
341Return whether the argument is a [`NewType`][typing.NewType].
343```pycon
344>>> UserId = NewType("UserId", int)
345>>> is_newtype(UserId)
346True
347```
348"""
350is_nodefault = _compile_identity_check_function('NoDefault', 'is_nodefault')
351is_nodefault.__doc__ = """
352Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object.
354```pycon
355>>> is_nodefault(NoDefault)
356True
357```
358"""
360is_noextraitems = _compile_identity_check_function('NoExtraItems', 'is_noextraitems')
361is_noextraitems.__doc__ = """
362Return whether the argument is the `NoExtraItems` sentinel object.
364```pycon
365>>> is_noextraitems(NoExtraItems)
366True
367```
368"""
370is_noreturn = _compile_identity_check_function('NoReturn', 'is_noreturn')
371is_noreturn.__doc__ = """
372Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][].
374```pycon
375>>> is_noreturn(NoReturn)
376True
377>>> is_noreturn(Never)
378False
379```
380"""
382is_notrequired = _compile_identity_check_function('NotRequired', 'is_notrequired')
383is_notrequired.__doc__ = """
384Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][].
386```pycon
387>>> is_notrequired(NotRequired)
388True
389```
390"""
392is_paramspecargs = _compile_isinstance_check_function('ParamSpecArgs', 'is_paramspecargs')
393is_paramspecargs.__doc__ = """
394Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs].
396```pycon
397>>> P = ParamSpec('P')
398>>> is_paramspecargs(P.args)
399True
400```
401"""
403is_paramspeckwargs = _compile_isinstance_check_function('ParamSpecKwargs', 'is_paramspeckwargs')
404is_paramspeckwargs.__doc__ = """
405Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs].
407```pycon
408>>> P = ParamSpec('P')
409>>> is_paramspeckwargs(P.kwargs)
410True
411```
412"""
414is_readonly = _compile_identity_check_function('ReadOnly', 'is_readonly')
415is_readonly.__doc__ = """
416Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][].
418```pycon
419>>> is_readonly(ReadOnly)
420True
421```
422"""
424is_required = _compile_identity_check_function('Required', 'is_required')
425is_required.__doc__ = """
426Return whether the argument is the [`Required`][typing.Required] [special form][].
428```pycon
429>>> is_required(Required)
430True
431```
432"""
434is_self = _compile_identity_check_function('Self', 'is_self')
435is_self.__doc__ = """
436Return whether the argument is the [`Self`][typing.Self] [special form][].
438```pycon
439>>> is_self(Self)
440True
441```
442"""
444# TYPE_CHECKING?
446is_typealias = _compile_identity_check_function('TypeAlias', 'is_typealias')
447is_typealias.__doc__ = """
448Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][].
450```pycon
451>>> is_typealias(TypeAlias)
452True
453```
454"""
456is_typeguard = _compile_identity_check_function('TypeGuard', 'is_typeguard')
457is_typeguard.__doc__ = """
458Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][].
460```pycon
461>>> is_typeguard(TypeGuard)
462True
463```
464"""
466is_typeis = _compile_identity_check_function('TypeIs', 'is_typeis')
467is_typeis.__doc__ = """
468Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][].
470```pycon
471>>> is_typeis(TypeIs)
472True
473```
474"""
476_is_typealiastype_inner = _compile_isinstance_check_function('TypeAliasType', '_is_typealiastype_inner')
479if _IS_PY310:
480 # Parameterized PEP 695 type aliases are instances of `types.GenericAlias` in typing_extensions>=4.13.0.
481 # On Python 3.10, with `Alias[int]` being such an instance of `GenericAlias`,
482 # `isinstance(Alias[int], TypeAliasType)` returns `True`.
483 # See https://github.com/python/cpython/issues/89828.
484 def is_typealiastype(obj: Any, /) -> 'TypeIs[TypeAliasType]':
485 return type(obj) is not GenericAlias and _is_typealiastype_inner(obj)
486else:
487 is_typealiastype = _compile_isinstance_check_function('TypeAliasType', 'is_typealiastype')
489is_typealiastype.__doc__ = """
490Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance.
492```pycon
493>>> type MyInt = int
494>>> is_typealiastype(MyInt)
495True
496>>> MyStr = TypeAliasType("MyStr", str)
497>>> is_typealiastype(MyStr):
498True
499>>> type MyList[T] = list[T]
500>>> is_typealiastype(MyList[int])
501False
502```
503"""
505is_unpack = _compile_identity_check_function('Unpack', 'is_unpack')
506is_unpack.__doc__ = """
507Return whether the argument is the [`Unpack`][typing.Unpack] [special form][].
509```pycon
510>>> is_unpack(Unpack)
511True
512>>> is_unpack(Unpack[Ts])
513False
514```
515"""
518if sys.version_info >= (3, 13):
520 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]':
521 return isinstance(obj, warnings.deprecated | typing_extensions.deprecated)
523else:
525 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]':
526 return isinstance(obj, typing_extensions.deprecated)
529is_deprecated.__doc__ = """
530Return whether the argument is a [`deprecated`][warnings.deprecated] instance.
532This also includes the [`typing_extensions` backport][typing_extensions.deprecated].
534```pycon
535>>> is_deprecated(warnings.deprecated('message'))
536True
537>>> is_deprecated(typing_extensions.deprecated('message'))
538True
539```
540"""
543# Aliases defined in the `typing` module using `typing._SpecialGenericAlias` (itself aliased as `alias()`):
544DEPRECATED_ALIASES: Final[dict[Any, type[Any]]] = {
545 typing.Hashable: collections.abc.Hashable,
546 typing.Awaitable: collections.abc.Awaitable,
547 typing.Coroutine: collections.abc.Coroutine,
548 typing.AsyncIterable: collections.abc.AsyncIterable,
549 typing.AsyncIterator: collections.abc.AsyncIterator,
550 typing.Iterable: collections.abc.Iterable,
551 typing.Iterator: collections.abc.Iterator,
552 typing.Reversible: collections.abc.Reversible,
553 typing.Sized: collections.abc.Sized,
554 typing.Container: collections.abc.Container,
555 typing.Collection: collections.abc.Collection,
556 # type ignore reason: https://github.com/python/typeshed/issues/6257:
557 typing.Callable: collections.abc.Callable, # pyright: ignore[reportAssignmentType, reportUnknownMemberType]
558 typing.AbstractSet: collections.abc.Set,
559 typing.MutableSet: collections.abc.MutableSet,
560 typing.Mapping: collections.abc.Mapping,
561 typing.MutableMapping: collections.abc.MutableMapping,
562 typing.Sequence: collections.abc.Sequence,
563 typing.MutableSequence: collections.abc.MutableSequence,
564 typing.Tuple: tuple,
565 typing.List: list,
566 typing.Deque: collections.deque,
567 typing.Set: set,
568 typing.FrozenSet: frozenset,
569 typing.MappingView: collections.abc.MappingView,
570 typing.KeysView: collections.abc.KeysView,
571 typing.ItemsView: collections.abc.ItemsView,
572 typing.ValuesView: collections.abc.ValuesView,
573 typing.Dict: dict,
574 typing.DefaultDict: collections.defaultdict,
575 typing.OrderedDict: collections.OrderedDict,
576 typing.Counter: collections.Counter,
577 typing.ChainMap: collections.ChainMap,
578 typing.Generator: collections.abc.Generator,
579 typing.AsyncGenerator: collections.abc.AsyncGenerator,
580 typing.Type: type,
581 # Defined in `typing.__getattr__`:
582 typing.Pattern: re.Pattern,
583 typing.Match: re.Match,
584 typing.ContextManager: contextlib.AbstractContextManager,
585 typing.AsyncContextManager: contextlib.AbstractAsyncContextManager,
586 # Skipped: `ByteString` (deprecated, removed in 3.14)
587}
588"""A mapping between the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/)."""
591# Add the `typing_extensions` aliases:
592for alias, target in list(DEPRECATED_ALIASES.items()):
593 # Use `alias.__name__` when we drop support for Python 3.9
594 if (te_alias := getattr(typing_extensions, alias._name, None)) is not None: 594 ↛ 592line 594 didn't jump to line 592 because the condition on line 594 was always true
595 DEPRECATED_ALIASES[te_alias] = target