Open
Description
I have a class which is generic and has some runtime marker for determining the underlying type. I would like to use that marker to guide MyPy in validating the implementation of the class. So if I have a TypeGuard which confirms the current object is generic in A (that is, TypeVar AB_T is bound to A), then it should be safe for the method which is annotated as returning an AB_T to return an A in that context.
What I'm finding instead is that the TypeGuard correctly tells MyPy that the object which knows itself to be Gen[AB_T] is specifically a Gen[A], but it does not make the link that AB_T is A. As such, it refuses to return an A.
I've tested the following example with MyPy 0.991 on Python 3.10
from typing import Generic, Literal, overload, TypeVar
from typing_extensions import TypeGuard
class A: ...
class B: ...
AB_T = TypeVar("AB_T", A, B)
class Gen(Generic[AB_T]):
# These two overloads ensure that the marker matches the generic type
@overload
def __init__(self: "Gen[A]", should_be_A: Literal[True]) -> None:
...
@overload
def __init__(self: "Gen[B]", should_be_A: Literal[False]) -> None:
...
def __init__(self: "Gen[AB_T]", should_be_A: bool) -> None:
self._should_be_A = should_be_A
def res(self) -> AB_T:
# This should return A() for a Gen[A] and B() for a Gen[B]
if Gen.gives_A(self): # This calling convention seems to be needed to narrow self
reveal_type(self) # and it now correctly understands that self is a Gen[A]
return A() # But this still complains about the scenario where self is a Gen[B]
elif Gen.gives_B(self):
reveal_type(self)
res: AB_T = B() # This also complains about the scenario where AB_T is A, even though we know it is B.
return res
else:
assert False
def gives_A(self) -> "TypeGuard[Gen[A]]":
return self._should_be_A
def gives_B(self) -> "TypeGuard[Gen[B]]":
return not self._should_be_A
Actual Behavior
generic_specialisation.py:26: note: Revealed type is "generic_specialisation.Gen[generic_specialisation.A]"
generic_specialisation.py:27: error: Incompatible return value type (got "A", expected "B") [return-value]
generic_specialisation.py:29: note: Revealed type is "generic_specialisation.Gen[generic_specialisation.B]"
generic_specialisation.py:30: error: Incompatible types in assignment (expression has type "B", variable has type "A") [assignment]
Metadata
Metadata
Assignees
Labels
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
JosiahKane commentedon Jan 10, 2023
In the event that this complaint is actually desired behaviour because in the general case TypeVars interact with variance, inheritance, and all sorts of "But what if it is actually both types?" questions, it may be worth knowing that the following (ugly) variation passes without errors by bouncing the variable through a member variable on the class.
A5rocks commentedon Jan 12, 2023
The TypeGuard PEP explicitly excludes narrowing self. I have a PR open that should warn on this behavior.
JosiahKane commentedon Jan 12, 2023
Thanks for the reply, @A5rocks. The only thing that I can see in PeP 647 is this point which near as I can tell explains why
self.gives_A()
doesn't act as a TypeGuard against self and why I needed to haveGen.gives_A(self)
instead. It does say "If narrowing of self or cls is required, the value can be passed as an explicit argument to a type guard function."My question, however, is whether when I have successfully (using the explicit argument approach) narrowed Self[T] to Self[A], other things in T like the return type of the current method should also be understood to be A.
It seems that either the answer is yes, and the [return-value] and [assignment] errors in my first post are false positives, or the answer is no and the indirection in my follow up post is a false negative.
A5rocks commentedon Jan 12, 2023
Ah I see what you mean now. (FWIW I recommend using
@staticmethod
on your typeguard, and yes I was talking about just the calling method, not that you can't call a typeguard onself
).I think yours might be the same issue as:
No
self
narrowing needed! I do think this is a bug and that this could be fixed (ie remapT
in the guards). Let's say the errors are false positives.FWIW this case would probably (by my guess) have to be fixed first:
I looked for an issue and found #1539
Reading the justification for closing it, your case is actually working as expected:tm:Actually nevermind, I think a big part of me being confused here is because while
isinstance(x, T)
means thatx
's type is a subtype ofT
, the typeguard will say thatx
's type isT
. This behavior is foot-gunny (#11230) but nonetheless your code should work. I remain convinced this is a false positive.TypeGuard
function microsoft/pyright#4448erictraut commentedon Jan 12, 2023
If you write a user-defined type guard function as an instance method, the
self
parameter is not considered the target of type narrowing. You need to provide at least one additional parameter beyondself
. @JosiahKane, in your original example above, you have definedgives_A
andgives_B
as instance methods, but you haven't provided a parameter for a type narrowing target. That means these are invalid user-defined type guard methods. Mypy should arguably generate an error in this case. I've just filed an enhancement request in pyright to do the same.If your intent is to test the value of a
Gen[Any]
object to determine whether it is either aGen[A]
orGen[B]
, then you should either move your type guard functions outside of the class or change these methods to static methods within the class.If, for some reason, you really don't want to use a static method and want to stick with an instance method, then you will need to define a second parameter.
And you would need to call it like this:
@A5rocks, you said "the typeguard will say that x's type is T". That's an incorrect interpretation. The type specified as the return type of a
TypeGuard
is expressed as a type annotation, and all type annotations implicitly mean "the type is a subtype ofT
". For example, if you specify that a function returns typefloat
, that doesn't mean that the return value is necessarily afloat
; it could be anint
. The same is true withTypeGuard[float]
.A5rocks commentedon Jan 12, 2023
Ah, my interpretation was based on how typeguards don't narrow to an intersection between the type it's guarded against and the type passed in, unlike normal type narrowing.
I see now how that was a wrong interpretation. I was confused at the time at how to handle the case described (assuming the typeguard is valid). Maybe this will become obvious once I think about it.
JosiahKane commentedon Jan 12, 2023
Thanks for the reply, @erictraut.
I understand the point about instance methods not narrowing self if used conventionally, which is why my code was written as
Gen.gives_A(self)
instead ofself.gives_A()
. I agree that it's then clearer to writegives_A
as a static method, and I would certainly not object if my version had raised a suitable warning. However, that's still not really the core question, and doesn't change the observed behaviour since the reveal_type lines show that both versions narrow self.The question is whether, having narrowed self as Gen[T] to Gen[A], MyPy should understand T to necessarily refer to A. If yes, then there is a false positive in complaining about returning A from a method which promises a T. If no, then there is a false negative in being able to return an A via the indirection of packing it into self, as illustrated in my second comment.
More helpful type guards (#14238)
ezyang commentedon Jan 29, 2024
I would quite like for this particular example to be fixed, it will make a lot of GADT-style code a lot simpler (today, I have to cast the return type back to T in order to get this to type check).
hauntsaninja commentedon Jan 30, 2024
@ezyang the example you post is not type safe:
ezyang commentedon Jan 30, 2024
Well, how about the more precise:
hauntsaninja commentedon Jan 30, 2024
That is safe (for non-generics)!
ezyang commentedon Jan 31, 2024
I am too used to type systems that don't have subtyping 👹
Update base for Update on "Some minor type stub improvements"
Update on "Some minor type stub improvements"
Some minor type stub improvements (#118529)
Some minor type stub improvements (#118529)
Update base for Update on "Some minor type stub improvements"
Update on "Some minor type stub improvements"
Some minor type stub improvements (#118529)
Some minor type stub improvements (#118529)
Some minor type stub improvements (#118529)
Some minor type stub improvements (#118529)
ReSqAr commentedon May 27, 2024
I just came across this issue and was indeed very puzzled by the mypy errors. It would be really great if the idea can be expressed safely using type annotations. I first thought that the type narrowing on
T
wasn't happening at all. However mypy complains (perhaps surprisingly?) about all three cases here:mypy's output is:
Version: