Type narrowing

Type checkers should narrow the types of expressions in certain contexts. This behavior is currently largely unspecified.

TypeGuard

(Originally specified in PEP 647.)

The symbol TypeGuard, exported from the typing module, is a special form that accepts a single type argument. It is used to annotate the return type of a user-defined type guard function. Return statements within a type guard function should return bool values, and type checkers should verify that all return paths return a bool.

TypeGuard is also valid as the return type of a callable, for example in callback protocols and in the Callable special form. In these contexts, it is treated as a subtype of bool. For example, Callable[..., TypeGuard[int]] is assignable to Callable[..., bool].

When TypeGuard is used to annotate the return type of a function or method that accepts at least one parameter, that function or method is treated by type checkers as a user-defined type guard. The type argument provided for TypeGuard indicates the type that has been validated by the function.

User-defined type guards can be generic functions, as shown in this example:

_T = TypeVar("_T")

def is_two_element_tuple(val: Tuple[_T, ...]) -> TypeGuard[tuple[_T, _T]]:
    return len(val) == 2

def func(names: tuple[str, ...]):
    if is_two_element_tuple(names):
        reveal_type(names)  # tuple[str, str]
    else:
        reveal_type(names)  # tuple[str, ...]

Type checkers should assume that type narrowing should be applied to the expression that is passed as the first positional argument to a user-defined type guard. If the type guard function accepts more than one argument, no type narrowing is applied to those additional argument expressions.

If a type guard function is implemented as an instance method or class method, the first positional argument maps to the second parameter (after “self” or “cls”).

Here are some examples of user-defined type guard functions that accept more than one argument:

def is_str_list(val: list[object], allow_empty: bool) -> TypeGuard[list[str]]:
    if len(val) == 0:
        return allow_empty
    return all(isinstance(x, str) for x in val)

_T = TypeVar("_T")

def is_set_of(val: set[Any], type: type[_T]) -> TypeGuard[Set[_T]]:
    return all(isinstance(x, type) for x in val)

The return type of a user-defined type guard function will normally refer to a type that is strictly “narrower” than the type of the first argument (that is, it’s a more specific type that can be assigned to the more general type). However, it is not required that the return type be strictly narrower. This allows for cases like the example above where list[str] is not assignable to list[object].

When a conditional statement includes a call to a user-defined type guard function, and that function returns true, the expression passed as the first positional argument to the type guard function should be assumed by a static type checker to take on the type specified in the TypeGuard return type, unless and until it is further narrowed within the conditional code block.

Some built-in type guards provide narrowing for both positive and negative tests (in both the if and else clauses). For example, consider the type guard for an expression of the form x is None. If x has a type that is a union of None and some other type, it will be narrowed to None in the positive case and the other type in the negative case. User-defined type guards apply narrowing only in the positive case (the if clause). The type is not narrowed in the negative case.

OneOrTwoStrs = tuple[str] | tuple[str, str]
def func(val: OneOrTwoStrs):
    if is_two_element_tuple(val):
        reveal_type(val)  # tuple[str, str]
        ...
    else:
        reveal_type(val)   # OneOrTwoStrs
        ...

    if not is_two_element_tuple(val):
        reveal_type(val)   # OneOrTwoStrs
        ...
    else:
        reveal_type(val)  # tuple[str, str]
        ...