Repository navigation
TypeGuard functions not properly recognizing generic types #11428
Copy link
Copy link
Closed
Labels
bugmypy got something wrongmypy got something wrongtopic-typeguard-typeisTypeGuard / TypeIs / PEP 647 / PEP 742TypeGuard / TypeIs / PEP 647 / PEP 742
Description
Bug Report
It seems the typing.TypeGuard support is not able to properly discriminate a union of two (generic) types.
To Reproduce
- The example below defines two completely distinct types,
Left[L]andRight[R], both of which happen to be generic and have a.valueattribute. - The
LeftOrRightis aUnionof those types. xis a variable of typeLeftOrRight[str, int]. In this example it has valueLeft("")- Using
isinstance()works fine to figure out whetherxis eitherLeftorRight, and within theif/elseblock the types are correct, including the typesLandR(bothTypeVar) - The
is_left()andis_right()helpers usetyping.TypeGuard, but Mypy does not recognize the types like it did with theisinstance()check.
from typing import Generic, TypeGuard, TypeVar, Union
L = TypeVar("L")
R = TypeVar("R")
class Left(Generic[L]):
value: L
def __init__(self, value: L):
self.value = value
class Right(Generic[R]):
value: R
def __init__(self, value: R):
self.value = value
LeftOrRight = Union[Left[L], Right[R]]
def is_left(obj: LeftOrRight[L, R]) -> TypeGuard[Left[L]]:
return isinstance(obj, Left)
def is_right(obj: LeftOrRight[L, R]) -> TypeGuard[Right[R]]:
return isinstance(obj, Right)
x: LeftOrRight[str, int] = Left("")
reveal_type(x) # Revealed type is "Union[Left[builtins.str], Right[builtins.int]]"
if isinstance(x, Left):
reveal_type(x) # Revealed type is "Left[builtins.str]"
x.value + "" # only valid for str
else:
reveal_type(x) # Revealed type is "Right[builtins.int]"
x.value + 10 # only valid for int
if is_left(x):
reveal_type(x) # Revealed type is "Left[L`-1]"
x.value + "" # Unsupported left operand type for + ("L") [operator]
else:
reveal_type(x) # Revealed type is "Union[Left[builtins.str], Right[builtins.int]]"
x.value + 10 # Unsupported operand types for + ("str" and "int") [operator]Expected Behavior
is_left() and is_right() act as type-safe ways to discriminate between the two members of the Union, including the actual type of the (generic) .value attribute.
Actual Behavior
reveal_type()returns wrong results- type errors reported in what seems to be perfectly fine and very strict code
Your Environment
This happens with both Mypy 0.910 (current release as time of writing) and today's git checkout of the main branch.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugmypy got something wrongmypy got something wrongtopic-typeguard-typeisTypeGuard / TypeIs / PEP 647 / PEP 742TypeGuard / TypeIs / PEP 647 / PEP 742
Activity
wbolster commentedon Nov 1, 2021
friendly ping 👋🏼 @hauntsaninja @ilevkivskyi, since you were involved with
TypeGuardrelated issues fairly recently, e.g. #11061, #10899, #10647, #11007erictraut commentedon Nov 1, 2021
The current behavior is correct and intended. User-defined type guards intentionally do not provide type narrowing for the negative ("else") case. To quote from PEP 647:
This decision was based on experience with user-defined type guards in TypeScript. It supports the negative case, and this leads to problems in a number of real-world cases. The TypeScript authors admitted that if they able to go back in time, they would probably reverse their decision. We learned from this when drafting PEP 647.
Some built-in type guards (such as
isinstancechecks) can be safely narrowed in the negative case because the semantics are well known. In the case of user-defined type guards, the semantics depend on the use case, and a type checker cannot assume that positive and negative checks are symmetrical. There are many examples where they are not.erictraut commentedon Nov 1, 2021
I was too hasty in my response. I think I misunderstood the problem you were reporting.
You are correct that mypy isn't correctly applying the solved type variables in the case of the type guard.
wbolster commentedon Nov 1, 2021
thanks for the response, @erictraut 🙏🏼
the lack of support for the negative case would account for the errors seen in the
elseclause of the example.however, the positive case also fails currently:
... even though the type should be fully know at this point:
xis eitherLeft[L]orRight[R], and the typeguard check should turn it intoLeft[L]. since the type ofLis known, namely astr, i would expect thatx.value + ""works without type errors.the
reveal_type()line seems to indicate this information is lost:...which would make
TypeGuardquite useless for generic types, specifically for things likeResult(which is what triggered me to look into this in the first place).erictraut commentedon Nov 1, 2021
Yes, I agree. You can address the negative case by adding an
elifcheck like I did in my last response, but that still leaves a problem.wbolster commentedon Nov 1, 2021
for full transparency: the problem i'm trying to solve is this one: rustedpy/result#69
...and it seems that this is not possible to achieve currently.
MicaelJarniac commentedon Dec 17, 2021
I believe I've stumbled upon the same issue, but when using
TypeGuardfromtyping_extensions: #117803ok commentedon Mar 24, 2022
Here is a simpler example, taken from PEP647. I ran it using
mypy v0.942in Python 3.8.I would have expected the revealed type to be
Set[str], which is correctly inferred bypyright.Fulguritude commentedon Mar 31, 2022
Same problem on my end, using mypy 0.941. FYI, I'm trying to write a generic
isinstance_builder(value, type_or_callback)which can either take in a type, or a custom isinstance-like callback of a single value.I even tried the (very simple) PEP647 example:
which returns
instead of the expected
Fixes generic inference in functions with `TypeGuard` (#11797)