Type narrowing¶
Type checkers should narrow the types of expressions incertain contexts. This behavior is currently largely unspecified.
TypeGuard¶
(Originally specified inPEP 647.)
The symbolTypeGuard, exported from thetyping module, is aspecial formthat accepts a single type argument. It is used to annotate the return type of auser-defined type guard function. Return statements within a type guard functionshould return bool values, and type checkers should verify that all return pathsreturn a bool.
TypeGuard is also valid as the return type of a callable, for examplein callback protocols and in theCallablespecial form. In thesecontexts, it is treated as a subtype of bool. For example,Callable[...,TypeGuard[int]]is assignable toCallable[...,bool].
WhenTypeGuard is used to annotate the return type of a function ormethod that accepts at least one parameter, that function or method istreated by type checkers as a user-defined type guard. The type argumentprovided forTypeGuard indicates the type that has been validated bythe function.
User-defined type guards can be generic functions, as shown in this example:
defis_two_element_tuple[T](val:tuple[T,...])->TypeGuard[tuple[T,T]]:returnlen(val)==2deffunc(names:tuple[str,...]):ifis_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 theexpression that is passed as the first positional argument to a user-definedtype guard. If the type guard function accepts more than one argument, notype 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 morethan one argument:
defis_str_list(val:list[object],allow_empty:bool)->TypeGuard[list[str]]:iflen(val)==0:returnallow_emptyreturnall(isinstance(x,str)forxinval)defis_set_of[T](val:set[Any],type:type[T])->TypeGuard[set[T]]:returnall(isinstance(x,type)forxinval)
The return type of a user-defined type guard function will normally refer toa type that is strictly “narrower” than the type of the first argument (thatis, 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. Thisallows for cases like the example above wherelist[str] is not assignabletolist[object].
When a conditional statement includes a call to a user-defined type guardfunction, and that function returns true, the expression passed as the firstpositional argument to the type guard function should be assumed by a statictype 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 negativetests (in both theif andelse clauses). For example, consider thetype guard for an expression of the formxisNone. Ifx has a type thatis a union of None and some other type, it will be narrowed toNone in thepositive case and the other type in the negative case. User-defined typeguards apply narrowing only in the positive case (theif clause). The typeis not narrowed in the negative case.
OneOrTwoStrs=tuple[str]|tuple[str,str]deffunc(val:OneOrTwoStrs):ifis_two_element_tuple(val):reveal_type(val)# tuple[str, str]...else:reveal_type(val)# OneOrTwoStrs...ifnotis_two_element_tuple(val):reveal_type(val)# OneOrTwoStrs...else:reveal_type(val)# tuple[str, str]...
TypeIs¶
(Originally specified inPEP 742.)
Thespecial formTypeIs is similar in usage, behavior, and runtimeimplementation asTypeGuard.
TypeIs accepts a single type argument and can be used as the return typeof a function. A function annotated as returning aTypeIs is called a“type narrowing function”. Type narrowing functions must returnboolvalues, and the type checker should verify that all return paths returnbool.
Type narrowing functions must accept at least one positional argument. The typenarrowing behavior is applied to the first positional argument passed tothe function. The function may accept additional arguments, but they arenot affected by type narrowing. If a type narrowing function is implemented asan instance method or class method, the first positional argument mapsto the second parameter (afterself orcls).
To specify the behavior ofTypeIs, we use the following terminology:
I =
TypeIsinput typeR =
TypeIsreturn typeA = Type of argument passed to type narrowing function (pre-narrowed)
NP = Narrowed type (positive; used when
TypeIsreturnedTrue)NN = Narrowed type (negative; used when
TypeIsreturnedFalse)defnarrower(x:I)->TypeIs[R]:...deffunc1(val:A):ifnarrower(val):assert_type(val,NP)else:assert_type(val,NN)
The return typeR must beassignable toI. The type checkershould emit an error if this condition is not met.
Formally, typeNP should be narrowed to\(A \land R\),the intersection ofA andR, and typeNN should be narrowed to\(A \land \neg R\), the intersection ofA and the complement ofR.In practice, the theoretic types for strict type guards cannot be expressedprecisely in the Python type system. Type checkers should fall back onpractical approximations of these types. As a rule of thumb, a type checkershould use the same type narrowing logic – and get results that are consistentwith – its handling ofisinstance(). This guidance allows for changesand improvements if the type system is extended in the future.
Type narrowing is applied in both the positive and negative case:
fromtypingimportTypeIs,assert_typedefis_str(x:object)->TypeIs[str]:returnisinstance(x,str)deff(x:str|int)->None:ifis_str(x):assert_type(x,str)else:assert_type(x,int)
The final narrowed type may be narrower thanR, due to the constraints of theargument’s previously-known type:
fromcollections.abcimportAwaitablefromtypingimportAny,TypeIs,assert_typeimportinspectdefisawaitable(x:object)->TypeIs[Awaitable[Any]]:returninspect.isawaitable(x)deff(x:Awaitable[int]|int)->None:ifisawaitable(x):# Type checkers may also infer the more precise type# "Awaitable[int] | (int & Awaitable[Any])"assert_type(x,Awaitable[int])else:assert_type(x,int)
It is an error to narrow to a type that is notassignable to the inputtype:
fromtypingimportTypeIsdefis_str(x:int)->TypeIs[str]:# Type checker error...
TypeIs is also valid as the return type of a callable, for examplein callback protocols and in theCallablespecial form. In thesecontexts, it is treated as a subtype of bool. For example,Callable[...,TypeIs[int]]is assignable toCallable[...,bool].
UnlikeTypeGuard,TypeIs is invariant in its argument type:TypeIs[B] is not a subtype ofTypeIs[A],even ifB is a subtype ofA.To see why, consider the following example:
deftakes_narrower(x:int|str,narrower:Callable[[object],TypeIs[int]]):ifnarrower(x):print(x+1)# x is an intelse:print("Hello "+x)# x is a strdefis_bool(x:object)->TypeIs[bool]:returnisinstance(x,bool)takes_narrower(1,is_bool)# Error: is_bool is not a TypeIs[int]
(Note thatbool is a subtype ofint.)This code fails at runtime, because the narrower returnsFalse (1 is not abool)and theelse branch is taken intakes_narrower().If the calltakes_narrower(1,is_bool) was allowed, type checkers would fail todetect this error.