This PEP is withdrawn. The Typing Council was unable to reach consensus onthis proposal, and the authors decided to withdraw it.
PEP 647 introduced the concept of a user-defined type guard function whichreturnsTrue if the type of the expression passed to its first parametermatches its returnTypeGuard type. For example, a function that has areturn type ofTypeGuard[str] is assumed to returnTrue if and only ifthe type of the expression passed to its first input parameter is astr.This allows type checkers to narrow types when a user-defined type guardfunction returnsTrue.
This PEP refines theTypeGuard mechanism introduced inPEP 647. Itallows type checkers to narrow types when a user-defined type guard functionreturnsFalse. It also allows type checkers to apply additional (moreprecise) type narrowing under certain circumstances when the type guardfunction returnsTrue.
User-defined type guard functions enable a type checker to narrow the type ofan expression when it is passed as an argument to the type guard function. TheTypeGuard mechanism introduced inPEP 647 is flexible, but thisflexibility imposes some limitations that developers have found inconvenientfor some uses.
Limitation 1: Type checkers are not allowed to narrow a type in the case wherethe type guard function returnsFalse. This means the type is not narrowedin the negative (“else”) clause.
Limitation 2: Type checkers must use theTypeGuard return type if the typeguard function returnsTrue regardless of whether additional narrowing canbe applied based on knowledge of the pre-narrowed type.
The following code sample demonstrates both of these limitations.
defis_iterable(val:object)->TypeGuard[Iterable[Any]]:returnisinstance(val,Iterable)deffunc(val:int|list[int]):ifis_iterable(val):# The type is narrowed to 'Iterable[Any]' as dictated by# the TypeGuard return typereveal_type(val)# Iterable[Any]else:# The type is not narrowed in the "False" casereveal_type(val)# int | list[int]# If "isinstance" is used in place of the user-defined type guard# function, the results differ because type checkers apply additional# logic for "isinstance"ifisinstance(val,Iterable):# Type is narrowed to "list[int]" because this is# a narrower (more precise) type than "Iterable[Any]"reveal_type(val)# list[int]else:# Type is narrowed to "int" because the logic eliminates# "list[int]" from the original unionreveal_type(val)# int
PEP 647 imposed these limitations so it could support use cases where thereturnTypeGuard type was not a subtype of the input type. Refer toPEP 647 for examples.
There are a number of issues where a stricterTypeGuard would havebeen a solution:
The use of a user-defined type guard function involves five types:
TypeGuard input typeTypeGuard return typedefguard(x:I)->TypeGuard[R]:...deffunc1(val:A):ifguard(val):reveal_type(val)# NPelse:reveal_type(val)# NN
This PEP proposes some modifications toPEP 647 to address the limitationsdiscussed above. These limitations are safe to eliminate only when a specificcondition is met. In particular, when the output typeR of a user-definedtype guard function is consistent[1] with the type of its firstinput parameter (I), type checkers should apply stricter type guardsemantics.
# Stricter type guard semantics are used in this case because# "Kangaroo | Koala" is consistent with "Animal"defis_marsupial(val:Animal)->TypeGuard[Kangaroo|Koala]:returnisinstance(val,Kangaroo|Koala)# Stricter type guard semantics are not used in this case because# "list[T]"" is not consistent with "list[T | None]"defhas_no_nones(val:list[T|None])->TypeGuard[list[T]]:returnNonenotinval
When stricter type guard semantics are applied, the application of auser-defined type guard function changes in two ways.
defis_str(val:str|int)->TypeGuard[str]:returnisinstance(val,str)deffunc(val:str|int):ifnotis_str(val):reveal_type(val)# int
defis_cardinal_direction(val:str)->TypeGuard[Literal["N","S","E","W"]]:returnvalin("N","S","E","W")deffunc(direction:Literal["NW","E"]):ifis_cardinal_direction(direction):reveal_type(direction)# "Literal[E]"else:reveal_type(direction)# "Literal[NW]"
The type-theoretic rules for type narrowing are specified in the followingtable.
| Non-strict type guard | Strict type guard | |
|---|---|---|
| Applies when | R not consistent with I | R consistent with I |
| NP is .. | R | A∧R |
| NN is .. | A | A∧¬R |
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 of “isinstance”. This guidance allows for changes andimprovements if the type system is extended in the future.
Any is consistent[1] with any other type, which meansstricter semantics can be applied.
# Stricter type guard semantics are used in this case because# "str" is consistent with "Any"defis_str(x:Any)->TypeGuard[str]:returnisinstance(x,str)deftest(x:float|str):ifis_str(x):reveal_type(x)# strelse:reveal_type(x)# float
This PEP proposes to change the existing behavior ofTypeGuard. This has noeffect at runtime, but it does change the types evaluated by a type checker.
defis_int(val:int|str)->TypeGuard[int]:returnisinstance(val,int)deffunc(val:int|str):ifis_int(val):reveal_type(val)# "int"else:reveal_type(val)# Previously "int | str", now "str"
This behavioral change results in different types evaluated by a type checker.It could therefore produce new (or mask existing) type errors.
Type checkers often improve narrowing logic or fix existing bugs in such logic,so users of static typing will be used to this type of behavioral change.
We also hypothesize that it is unlikely that existing typed Python code relieson the current behavior ofTypeGuard. To validate our hypothesis, weimplemented the proposed change in pyright and ran this modified version onroughly 25 typed code bases usingmypy primer to see if there were anydifferences in the output. As predicted, the behavioral change had minimalimpact. The only noteworthy change was that some#type:ignore commentswere no longer necessary, indicating that these code bases were already workingaround the existing limitations ofTypeGuard.
It is possible for a user-defined type guard function to rely on the oldbehavior. Such type guard functions could break with the new behavior.
defis_positive_int(val:int|str)->TypeGuard[int]:returnisinstance(val,int)andval>0deffunc(val:int|str):ifis_positive_int(val):reveal_type(val)# "int"else:# With the older behavior, the type of "val" is evaluated as# "int | str"; with the new behavior, the type is narrowed to# "str", which is perhaps not what was intended.reveal_type(val)
We think it is unlikely that such user-defined type guards exist in real-worldcode. The mypy primer results didn’t uncover any such cases.
Users unfamiliar withTypeGuard are likely to expect the behavior outlinedin this PEP, therefore makingTypeGuard easier to teach and explain.
A referenceimplementation of this idea exists in pyright.
To enable the modified behavior, the configuration flagenableExperimentalFeatures must be set to true. This can be done on aper-file basis by adding a comment:
# pyright: enableExperimentalFeatures=trueA newStrictTypeGuard construct was proposed. This alternative form wouldbe similar to aTypeGuard except it would apply stricter type guardsemantics. It would also enforce that the return type was consistent[1] with the input type. See this thread for details:StrictTypeGuard proposal
This idea was rejected because it is unnecessary in most cases and addedunnecessary complexity. It would require the introduction of a new specialform, and developers would need to be educated about the subtle differencebetween the two forms.
Another idea was proposed whereTypeGuard could support a second optionaltype argument that indicates the type that should be used for narrowing in thenegative (“else”) case.
defis_int(val:int|str)->TypeGuard[int,str]:returnisinstance(val,int)
This idea was proposedhere.
It was rejected because it was considered too complicated and addressed onlyone of the two main limitations ofTypeGuard. Refer to thisthread forthe full discussion.
This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0724.rst
Last modified:2025-02-01 08:55:40 GMT