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

1"""Low-level introspection utilities for [`typing`][] members. 

2 

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 

7 

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 

17 

18import typing_extensions 

19from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated 

20 

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) 

56 

57_IS_PY310 = sys.version_info[:2] == (3, 10) 

58 

59 

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

62 

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: 

66 

67 ```pycon 

68 >>> from typing import Literal as t_Literal 

69 >>> from typing_extensions import Literal as te_Literal, get_origin 

70 

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) 

81 

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' 

93 

94 func_code = dedent(f""" 

95 def {function_name}(obj: Any, /) -> bool: 

96 return {check_code} 

97 """) 

98 

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] 

103 

104 

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

107 

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) 

113 

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' 

125 

126 func_code = dedent(f""" 

127 def {function_name}(obj: Any, /) -> 'TypeIs[{member}]': 

128 return {check_code} 

129 """) 

130 

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] 

135 

136 

137# Keep this ordered, as per `typing.__all__`: 

138 

139is_annotated = _compile_identity_check_function('Annotated', 'is_annotated') 

140is_annotated.__doc__ = """ 

141Return whether the argument is the [`Annotated`][typing.Annotated] [special form][]. 

142 

143```pycon 

144>>> is_annotated(Annotated) 

145True 

146>>> is_annotated(Annotated[int, ...]) 

147False 

148``` 

149""" 

150 

151is_any = _compile_identity_check_function('Any', 'is_any') 

152is_any.__doc__ = """ 

153Return whether the argument is the [`Any`][typing.Any] [special form][]. 

154 

155```pycon 

156>>> is_any(Any) 

157True 

158``` 

159""" 

160 

161is_classvar = _compile_identity_check_function('ClassVar', 'is_classvar') 

162is_classvar.__doc__ = """ 

163Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][]. 

164 

165```pycon 

166>>> is_classvar(ClassVar) 

167True 

168>>> is_classvar(ClassVar[int]) 

169>>> False 

170``` 

171""" 

172 

173is_concatenate = _compile_identity_check_function('Concatenate', 'is_concatenate') 

174is_concatenate.__doc__ = """ 

175Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][]. 

176 

177```pycon 

178>>> is_concatenate(Concatenate) 

179True 

180>>> is_concatenate(Concatenate[int, P]) 

181False 

182``` 

183""" 

184 

185is_final = _compile_identity_check_function('Final', 'is_final') 

186is_final.__doc__ = """ 

187Return whether the argument is the [`Final`][typing.Final] [type qualifier][]. 

188 

189```pycon 

190>>> is_final(Final) 

191True 

192>>> is_final(Final[int]) 

193False 

194``` 

195""" 

196 

197 

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

204 

205```pycon 

206>>> is_forwardref(ForwardRef('T')) 

207True 

208``` 

209""" 

210 

211 

212is_generic = _compile_identity_check_function('Generic', 'is_generic') 

213is_generic.__doc__ = """ 

214Return whether the argument is the [`Generic`][typing.Generic] [special form][]. 

215 

216```pycon 

217>>> is_generic(Generic) 

218True 

219>>> is_generic(Generic[T]) 

220False 

221``` 

222""" 

223 

224is_literal = _compile_identity_check_function('Literal', 'is_literal') 

225is_literal.__doc__ = """ 

226Return whether the argument is the [`Literal`][typing.Literal] [special form][]. 

227 

228```pycon 

229>>> is_literal(Literal) 

230True 

231>>> is_literal(Literal["a"]) 

232False 

233``` 

234""" 

235 

236 

237# `get_origin(Optional[int]) is Union`, so `is_optional()` isn't implemented. 

238 

239is_paramspec = _compile_isinstance_check_function('ParamSpec', 'is_paramspec') 

240is_paramspec.__doc__ = """ 

241Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec]. 

242 

243```pycon 

244>>> P = ParamSpec('P') 

245>>> is_paramspec(P) 

246True 

247``` 

248""" 

249 

250# Protocol? 

251 

252is_typevar = _compile_isinstance_check_function('TypeVar', 'is_typevar') 

253is_typevar.__doc__ = """ 

254Return whether the argument is an instance of [`TypeVar`][typing.TypeVar]. 

255 

256```pycon 

257>>> T = TypeVar('T') 

258>>> is_typevar(T) 

259True 

260``` 

261""" 

262 

263is_typevartuple = _compile_isinstance_check_function('TypeVarTuple', 'is_typevartuple') 

264is_typevartuple.__doc__ = """ 

265Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple]. 

266 

267```pycon 

268>>> Ts = TypeVarTuple('Ts') 

269>>> is_typevartuple(Ts) 

270True 

271``` 

272""" 

273 

274is_union = _compile_identity_check_function('Union', 'is_union') 

275is_union.__doc__ = """ 

276Return whether the argument is the [`Union`][typing.Union] [special form][]. 

277 

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]`. 

280 

281```pycon 

282>>> is_union(Union) 

283True 

284>>> is_union(Union[int, str]) 

285False 

286``` 

287 

288!!! warning 

289 This does not check for unions using the [new syntax][types-union] (e.g. `int | str`). 

290""" 

291 

292 

293def is_namedtuple(obj: Any, /) -> bool: 

294 """Return whether the argument is a named tuple type. 

295 

296 This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the 

297 [`collections.namedtuple`][] factory function. 

298 

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] 

313 

314 

315# TypedDict? 

316 

317# BinaryIO? IO? TextIO? 

318 

319is_literalstring = _compile_identity_check_function('LiteralString', 'is_literalstring') 

320is_literalstring.__doc__ = """ 

321Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][]. 

322 

323```pycon 

324>>> is_literalstring(LiteralString) 

325True 

326``` 

327""" 

328 

329is_never = _compile_identity_check_function('Never', 'is_never') 

330is_never.__doc__ = """ 

331Return whether the argument is the [`Never`][typing.Never] [special form][]. 

332 

333```pycon 

334>>> is_never(Never) 

335True 

336``` 

337""" 

338 

339is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype') 

340is_newtype.__doc__ = """ 

341Return whether the argument is a [`NewType`][typing.NewType]. 

342 

343```pycon 

344>>> UserId = NewType("UserId", int) 

345>>> is_newtype(UserId) 

346True 

347``` 

348""" 

349 

350is_nodefault = _compile_identity_check_function('NoDefault', 'is_nodefault') 

351is_nodefault.__doc__ = """ 

352Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object. 

353 

354```pycon 

355>>> is_nodefault(NoDefault) 

356True 

357``` 

358""" 

359 

360is_noextraitems = _compile_identity_check_function('NoExtraItems', 'is_noextraitems') 

361is_noextraitems.__doc__ = """ 

362Return whether the argument is the `NoExtraItems` sentinel object. 

363 

364```pycon 

365>>> is_noextraitems(NoExtraItems) 

366True 

367``` 

368""" 

369 

370is_noreturn = _compile_identity_check_function('NoReturn', 'is_noreturn') 

371is_noreturn.__doc__ = """ 

372Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][]. 

373 

374```pycon 

375>>> is_noreturn(NoReturn) 

376True 

377>>> is_noreturn(Never) 

378False 

379``` 

380""" 

381 

382is_notrequired = _compile_identity_check_function('NotRequired', 'is_notrequired') 

383is_notrequired.__doc__ = """ 

384Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][]. 

385 

386```pycon 

387>>> is_notrequired(NotRequired) 

388True 

389``` 

390""" 

391 

392is_paramspecargs = _compile_isinstance_check_function('ParamSpecArgs', 'is_paramspecargs') 

393is_paramspecargs.__doc__ = """ 

394Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs]. 

395 

396```pycon 

397>>> P = ParamSpec('P') 

398>>> is_paramspecargs(P.args) 

399True 

400``` 

401""" 

402 

403is_paramspeckwargs = _compile_isinstance_check_function('ParamSpecKwargs', 'is_paramspeckwargs') 

404is_paramspeckwargs.__doc__ = """ 

405Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs]. 

406 

407```pycon 

408>>> P = ParamSpec('P') 

409>>> is_paramspeckwargs(P.kwargs) 

410True 

411``` 

412""" 

413 

414is_readonly = _compile_identity_check_function('ReadOnly', 'is_readonly') 

415is_readonly.__doc__ = """ 

416Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][]. 

417 

418```pycon 

419>>> is_readonly(ReadOnly) 

420True 

421``` 

422""" 

423 

424is_required = _compile_identity_check_function('Required', 'is_required') 

425is_required.__doc__ = """ 

426Return whether the argument is the [`Required`][typing.Required] [special form][]. 

427 

428```pycon 

429>>> is_required(Required) 

430True 

431``` 

432""" 

433 

434is_self = _compile_identity_check_function('Self', 'is_self') 

435is_self.__doc__ = """ 

436Return whether the argument is the [`Self`][typing.Self] [special form][]. 

437 

438```pycon 

439>>> is_self(Self) 

440True 

441``` 

442""" 

443 

444# TYPE_CHECKING? 

445 

446is_typealias = _compile_identity_check_function('TypeAlias', 'is_typealias') 

447is_typealias.__doc__ = """ 

448Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][]. 

449 

450```pycon 

451>>> is_typealias(TypeAlias) 

452True 

453``` 

454""" 

455 

456is_typeguard = _compile_identity_check_function('TypeGuard', 'is_typeguard') 

457is_typeguard.__doc__ = """ 

458Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][]. 

459 

460```pycon 

461>>> is_typeguard(TypeGuard) 

462True 

463``` 

464""" 

465 

466is_typeis = _compile_identity_check_function('TypeIs', 'is_typeis') 

467is_typeis.__doc__ = """ 

468Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][]. 

469 

470```pycon 

471>>> is_typeis(TypeIs) 

472True 

473``` 

474""" 

475 

476_is_typealiastype_inner = _compile_isinstance_check_function('TypeAliasType', '_is_typealiastype_inner') 

477 

478 

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') 

488 

489is_typealiastype.__doc__ = """ 

490Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance. 

491 

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""" 

504 

505is_unpack = _compile_identity_check_function('Unpack', 'is_unpack') 

506is_unpack.__doc__ = """ 

507Return whether the argument is the [`Unpack`][typing.Unpack] [special form][]. 

508 

509```pycon 

510>>> is_unpack(Unpack) 

511True 

512>>> is_unpack(Unpack[Ts]) 

513False 

514``` 

515""" 

516 

517 

518if sys.version_info >= (3, 13): 

519 

520 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': 

521 return isinstance(obj, warnings.deprecated | typing_extensions.deprecated) 

522 

523else: 

524 

525 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': 

526 return isinstance(obj, typing_extensions.deprecated) 

527 

528 

529is_deprecated.__doc__ = """ 

530Return whether the argument is a [`deprecated`][warnings.deprecated] instance. 

531 

532This also includes the [`typing_extensions` backport][typing_extensions.deprecated]. 

533 

534```pycon 

535>>> is_deprecated(warnings.deprecated('message')) 

536True 

537>>> is_deprecated(typing_extensions.deprecated('message')) 

538True 

539``` 

540""" 

541 

542 

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/).""" 

589 

590 

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