Python TypeGuard vs TypeIs: Understanding PEP 742

Python 3.13 introduced typing.TypeIs via PEP 742 to solve long-standing type-narrowing limitations found in typing.TypeGuard (introduced in PEP 647). While both constructs allow developers to write user-defined type guard functions that inspect objects at runtime, they handle conditional branching differently. TypeGuard only narrows types in the positive (if) branch and permits arbitrary type overrides, whereas TypeIs enforces stricter subtyping rules and provides symmetric narrowing across both the positive (if) and negative (else) branches.

The Limitations of TypeGuard

Introduced in Python 3.10, TypeGuard allows functions to return a boolean indicating whether an argument matches a specific type. However, it suffers from two major limitations:

  1. No Negative Narrowing: When a TypeGuard returns False, type checkers cannot infer anything about the type in the else branch. The variable retains its original, un-narrowed type.
  2. Unsafe Type Overrides: TypeGuard does not require the narrowed type to be a subtype of the input type. If a function accepts object and asserts TypeGuard[int], the type checker blindly treats the object as int in the if block, even if the runtime logic is flawed.
from typing import TypeGuard

def is_string(val: object) -> TypeGuard[str]:
    return isinstance(val, str)

def process(val: str | int) -> None:
    if is_string(val):
        reveal_type(val)  # str
    else:
        reveal_type(val)  # str | int (TypeGuard cannot eliminate str here)

In the example above, even though val can only be int when is_string(val) is False, the type checker still sees str | int.

How TypeIs Resolves These Issues

TypeIs addresses these shortcomings by modeling how built-in checks like isinstance() actually work.

1. Symmetric Narrowing (Positive and Negative Branches)

When a function returning TypeIs[T] yields True, the variable is narrowed to T. When it yields False, the type checker removes T from the candidate union in the else branch.

from typing import TypeIs

def is_string(val: object) -> TypeIs[str]:
    return isinstance(val, str)

def process(val: str | int) -> None:
    if is_string(val):
        reveal_type(val)  # str
    else:
        reveal_type(val)  # int (str is safely removed from the union)

2. Strict Subtyping Requirement

Unlike TypeGuard, TypeIs[T] requires T to be consistent with the input type of the inspected argument. The narrowed type is computed as the intersection of the original type and T. If T cannot be a subtype or specialization of the input type, static type checkers like Pyright or Mypy will emit an error.

Key Differences Summary

Feature typing.TypeGuard typing.TypeIs
PEP Specification PEP 647 (Python 3.10) PEP 742 (Python 3.13)
Positive Narrowing (if) Replaces type with T Narrows via intersection of input and T
Negative Narrowing (else) No narrowing occurs Eliminates T from the union
Subtyping Constraint Not required (allows unsafe overrides) Required (must be compatible with input type)

When to Use Which