@@ -389,3 +389,169 @@ or rewrite the function to be slightly more verbose:
389
389
elif b is not None :
390
390
return b
391
391
return C()
392
+
393
+
394
+ .. _typeis :
395
+
396
+ TypeIs
397
+ ------
398
+
399
+ Mypy supports TypeIs (:pep: `742 `).
400
+
401
+ A `TypeIs narrowing function <https://typing.readthedocs.io/en/latest/spec/narrowing.html#typeis >`_
402
+ allows you to define custom type checks that can narrow the type of a variable
403
+ in `both the if and else <https://docs.python.org/3.13/library/typing.html#typing.TypeIs>_ `
404
+ branches of a conditional, similar to how the built-in isinstance() function works.
405
+
406
+ TypeIs is new in Python 3.13 — for use in older Python versions, use the backport
407
+ from `typing_extensions <https://typing-extensions.readthedocs.io/en/latest/>_ `
408
+
409
+ Consider the following example using TypeIs:
410
+
411
+ .. code-block :: python
412
+
413
+ from typing import TypeIs
414
+
415
+ def is_str (x : object ) -> TypeIs[str ]:
416
+ return isinstance (x, str )
417
+
418
+ def process (x : int | str ) -> None :
419
+ if is_str(x):
420
+ reveal_type(x) # Revealed type is 'str'
421
+ print (x.upper()) # Valid: x is str
422
+ else :
423
+ reveal_type(x) # Revealed type is 'int'
424
+ print (x + 1 ) # Valid: x is int
425
+
426
+ In this example, the function is_str is a type narrowing function
427
+ that returns TypeIs[str]. When used in an if statement, x is narrowed
428
+ to str in the if branch and to int in the else branch.
429
+
430
+ Key points:
431
+
432
+
433
+ - The function must accept at least one positional argument.
434
+
435
+ - The return type is annotated as ``TypeIs[T] ``, where ``T `` is the type you
436
+ want to narrow to.
437
+
438
+ - The function must return a ``bool `` value.
439
+
440
+ - In the ``if `` branch (when the function returns ``True ``), the type of the
441
+ argument is narrowed to the intersection of its original type and ``T ``.
442
+
443
+ - In the ``else `` branch (when the function returns ``False ``), the type of
444
+ the argument is narrowed to the intersection of its original type and the
445
+ complement of ``T ``.
446
+
447
+
448
+ TypeIs vs TypeGuard
449
+ ~~~~~~~~~~~~~~~~~~~
450
+
451
+ While both TypeIs and TypeGuard allow you to define custom type narrowing
452
+ functions, they differ in important ways:
453
+
454
+ - **Type narrowing behavior **: TypeIs narrows the type in both the if and else branches,
455
+ whereas TypeGuard narrows only in the if branch.
456
+
457
+ - **Compatibility requirement **: TypeIs requires that the narrowed type T be
458
+ compatible with the input type of the function. TypeGuard does not have this restriction.
459
+
460
+ - **Type inference **: With TypeIs, the type checker may infer a more precise type by
461
+ combining existing type information with T.
462
+
463
+ Here's an example demonstrating the behavior with TypeGuard:
464
+
465
+ .. code-block :: python
466
+
467
+ from typing import TypeGuard, reveal_type
468
+
469
+ def is_str (x : object ) -> TypeGuard[str ]:
470
+ return isinstance (x, str )
471
+
472
+ def process (x : int | str ) -> None :
473
+ if is_str(x):
474
+ reveal_type(x) # Revealed type is "builtins.str"
475
+ print (x.upper()) # ok: x is str
476
+ else :
477
+ reveal_type(x) # Revealed type is "Union[builtins.int, builtins.str]"
478
+ print (x + 1 ) # ERROR: Unsupported operand types for + ("str" and "int") [operator]
479
+
480
+ Generic TypeIs
481
+ ~~~~~~~~~~~~~~
482
+
483
+ ``TypeIs `` functions can also work with generic types:
484
+
485
+ .. code-block :: python
486
+
487
+ from typing import TypeVar, TypeIs
488
+
489
+ T = TypeVar(' T' )
490
+
491
+ def is_two_element_tuple (val : tuple[T, ... ]) -> TypeIs[tuple[T, T]]:
492
+ return len (val) == 2
493
+
494
+ def process (names : tuple[str , ... ]) -> None :
495
+ if is_two_element_tuple(names):
496
+ reveal_type(names) # Revealed type is 'tuple[str, str]'
497
+ else :
498
+ reveal_type(names) # Revealed type is 'tuple[str, ...]'
499
+
500
+
501
+ TypeIs with Additional Parameters
502
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
503
+ TypeIs functions can accept additional parameters beyond the first.
504
+ The type narrowing applies only to the first argument.
505
+
506
+ .. code-block :: python
507
+
508
+ from typing import Any, TypeVar, reveal_type, TypeIs
509
+
510
+ T = TypeVar(' T' )
511
+
512
+ def is_instance_of (val : Any, typ : type[T]) -> TypeIs[T]:
513
+ return isinstance (val, typ)
514
+
515
+ def process (x : Any) -> None :
516
+ if is_instance_of(x, int ):
517
+ reveal_type(x) # Revealed type is 'int'
518
+ print (x + 1 ) # ok
519
+ else :
520
+ reveal_type(x) # Revealed type is 'Any'
521
+
522
+ TypeIs in Methods
523
+ ~~~~~~~~~~~~~~~~~
524
+
525
+ A method can also serve as a ``TypeIs `` function. Note that in instance or
526
+ class methods, the type narrowing applies to the second parameter
527
+ (after ``self `` or ``cls ``).
528
+
529
+ .. code-block :: python
530
+
531
+ class Validator :
532
+ def is_valid (self , instance : object ) -> TypeIs[str ]:
533
+ return isinstance (instance, str )
534
+
535
+ def process (self , to_validate : object ) -> None :
536
+ if Validator().is_valid(to_validate):
537
+ reveal_type(to_validate) # Revealed type is 'str'
538
+ print (to_validate.upper()) # ok: to_validate is str
539
+
540
+
541
+ Assignment Expressions with TypeIs
542
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
543
+
544
+ You can use the assignment expression operator ``:= `` with ``TypeIs `` to create a new variable and narrow its type simultaneously.
545
+
546
+ .. code-block :: python
547
+
548
+ from typing import TypeIs, reveal_type
549
+
550
+ def is_float (x : object ) -> TypeIs[float ]:
551
+ return isinstance (x, float )
552
+
553
+ def main (a : object ) -> None :
554
+ if is_float(x := a):
555
+ reveal_type(x) # Revealed type is 'float'
556
+ # x is narrowed to float in this block
557
+ print (x + 1.0 )
0 commit comments