You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Unless I'm mistaken, TypeGuard and bool aren't type compatible (the PEP said so), so wouldn't replacing those functions return types be breaking changes, and only new functions can be type guards?
I think there was some bad interplay with overload type overlapping that necessitated them being unique (e.g. conditions where a function overload couldn't have a more specific return in the case of a TypeGuard parameter).
@gvanrossum or @erictraut may remember; I wasn't there for the discussion and I don't want to go too far into second-hand explanation past citing the PEP.
Testing this, mypy and pyright seem to disagree. I wrote this code:
from typing import Callable, Any
from typing_extensions import TypeGuard
class SomeType:
...
def guard(x: Any) -> TypeGuard[SomeType]:
return True
def hof(func: Callable[[Any], bool]):
...
hof(guard)
def notguard(x: Any) -> bool:
...
def hof2(func: Callable[[Any], TypeGuard[SomeType]]):
...
hof2(notguard)
Mypy accepts this, but pyright gives errors on the hof(guard) and the hof2(notguard) calls:
C:\Users\gvanrossum\cpython\guard.py:13:5 - error: Argument of type "(x: Any) -> TypeGuard[SomeType]" cannot be assigned to parameter "func" of type "(_p0: Any) -> bool" in function "hof"
Type "(x: Any) -> TypeGuard[SomeType]" cannot be assigned to type "(_p0: Any) -> bool"
Function return type "TypeGuard[SomeType]" is incompatible with type "bool"
"TypeGuard[SomeType]" is incompatible with "bool" (reportGeneralTypeIssues)
C:\Users\gvanrossum\cpython\guard.py:21:6 - error: Argument of type "(x: Any) -> bool" cannot be assigned to parameter "func" of type "(_p0: Any) -> TypeGuard[SomeType]" in function "hof2"
Type "(x: Any) -> bool" cannot be assigned to type "(_p0: Any) -> TypeGuard[SomeType]"
Function return type "bool" is incompatible with type "TypeGuard[SomeType]"
"bool" is incompatible with "TypeGuard[SomeType]" (reportGeneralTypeIssues)
In all other respects, TypeGuard is a distinct type from bool. It is not a subtype of bool. Therefore, Callable[..., TypeGuard[int]] is not assignable to Callable[..., bool].
I added this to the spec at your recommendation, Guido, because you argued (convincingly) that there were cases where we would want to distinguish between bool and TypeGuard[T] in overloads.
So it appears that mypy is not conforming to the PEP.
Thanks for the explanation! It's very limiting though if we can't safely change existing functions like inspect.is* to use TypeGuards. Do you see a way to allow that?
As a user I'd expect Guido's hof(guard) example to succeed but hof2(notguard) to fail. Treating TypeGuard as a subtype of bool would do that.
Unfortunately if we create any equivalency between bool and TypeGuard[T],
we'd lose the ability to use them as distinct in overloads. E.g.
```
@overload
def filter(f: Callable[[Any], TypeGuard[T]], Iterable[Any]) -> Iterable[T]:
...
@overload
def filter(f: Callable[[T], bool], Iterable[T]) -> Iterable[T]: ...
```
I tried to create some concrete examples to show this back in the "final call for comments" thread in relation to python/typing#253 (comment), and some seemed to think that this restriction was unnecessary, but I gave up thinking about it after getting berated enough to not want to continue participating in the thread anymore.
If TypeGuard is treated as a subtype of bool, the overloads should work, as long as the more specific overload (TypeGuard) is listed first. We have quite a few cases in typeshed where we do that. (Not with TypeGuard obviously.)
pyright doesn't actually support Guido's example, but it does infer x to be of type TypeGuard[] and prevent you from passing x to a function that takes a bool.
Stepping back, the reason I'm pushing for a change here is that it would be unfortunate if we can't take advantage of TypeGuard on any existing functions. Users who want narrowing from functions like inspect.isfunction would have to use a copy of the function that returns a TypeGuard instead. That's not an outcome I'd like to see. Of course, I should have brought this up while the PEP was under discussion instead of now; sorry for missing it.
I still think that making TypeGuard a subclass of bool would work well in practice. There are some soundness problems, but those seem mostly theoretical. I have a hard time imagining the unsoundness manifesting as anything worse than a missed narrowing. I'm open to other solutions though.
Hm, then what's the point of inferring TypeGuard instead of bool?
TypeGuard[List[str]] is the type of the expression, and assigning it just uses that type. Transparently converting it to bool in specific circumstances sounds like it would cause oddities (as "getting the type" of this call is the same operation if you were to want to use it by passing it to something that accepts a type guard, like a potential filter function).
Of course, if TypeGuard were compatible in some sense, then how this is chosen wouldn't really matter, except maybe that variables may be inferred to have a type that's more specific than what a user wants, potentially (but that's nothing new).
TypeGuard[List[str]] is the type of the expression
I guess in our minds we think of this fundamentally differently. To me, TypeGuard[List[str]] is special syntax that adds an attribute to the function outside the type system proper, while its return type is just bool.
If you consider the TypeScript equivalent, arg is SomeType, I'd say that clearly the return type is not arg is SomeType, the return type is boolean, and the special syntax makes the function special in certain contexts.
In Python, TypeGuard[...] is not a generic type, it just looks like one because that's pretty much the only syntax we have available.
I confirmed that there was a bug in pyright's implementation when evaluating the return type of a call to a user-defined type guard. It should evaluate the return type as bool, not as TypeGuard[T]. That bug will be fixed in the next release.
The question that is being raised here is whether a function that accepts a callback of type Callable[..., bool] should accept a user-defined type guard function. I also see no reason why it shouldn't, since the runtime return type of such a function is bool. I've made the change in pyright to allow this, and it will be in the next release also.
I think PyCharm should mostly be considered a "soft blocker". While we shouldn't break central, often used functions, adding TypeGuard to less often used modules shouldn't be a problem. The worst that PyCharm does is to warn about something it doesn't understand. As a PyCharm user I'm quite used to a fair amount of warnings, unfortunately.
I just noticed that there's still a small difference between the behavior of mypy and pyright. Mypy apparently allows non-type-guard functions (those that return a bool) to be passed to a function that indicates it requires a type guard function. This can be seen using Guido's test code above.
defnotguard(x: Any) ->bool: ...
defhof2(func: Callable[[Any], TypeGuard[SomeType]]): ...
hof2(notguard) # Pyright flags this as an error, mypy does not
@gvanrossum, can you explain why you think this should not be an error? Is there a compelling reason to support this? I think it creates some problems, specifically for overload matching. It also creates problems for generics used in TypeGuards. For example, what would you expect the return result to be for the following call?
defmy_filter(func: Callable[[Any], TypeGuard[T]]) ->List[T]: ...
my_filter(notguard) # What is the return type of this call? List[Any]?
Maybe it's OK for mypy and pyright to differ in this specific use case, but I want to make you all aware of this because pyright's current behavior (with my recent changes noted above) requires that any use of TypeGuard callbacks in typeshed must be done with overloads, and the TypeGuard overload must appear prior to the non-TypeGuard overload, like this:
I wasn’t expressing an opinion on what is correct, just observing a difference. I don’t think I will get to changing mypy even if it is decided it’s wrong, but someone else might. Sorry!
I agree that the mypy behavior Eric highlighted doesn't make sense, so I opened an issue about it. With Sebastian's comment on PyCharm, I think we're OK to start using TypeGuard in typeshed, so I'm closing this issue.
Bumped pyright's locked version to 1.1.140, which includes the aforementioned fixes.
Happy to know that the difference was a bug; I probably should have known better when I went to TS, wrote the same type guard, and the hover said boolean, not something special... 🙂
Activity
JelleZijlstra commentedon May 11, 2021
@sproshev does PyCharm support TypeGuard already?
sproshev commentedon May 11, 2021
Not even planned :( It is going to be released in Python 3.10, right?
JelleZijlstra commentedon May 11, 2021
That's right. It's probably pretty simple to get enough support to allow TypeGuard in typeshed: just interpret
TypeGuard[...]as an alias for bool.JelleZijlstra commentedon May 12, 2021
Some examples of functions where this could be useful:
builtins.callableshould returnTypeGuard[Callable[..., Any]]inspectlikeinspect.isfunction,isclass, etc.jakebailey commentedon May 12, 2021
Unless I'm mistaken,
TypeGuardandboolaren't type compatible (the PEP said so), so wouldn't replacing those functions return types be breaking changes, and only new functions can be type guards?hauntsaninja commentedon May 12, 2021
(Why does the PEP say so?)
jakebailey commentedon May 12, 2021
I think there was some bad interplay with overload type overlapping that necessitated them being unique (e.g. conditions where a function overload couldn't have a more specific return in the case of a TypeGuard parameter).
@gvanrossum or @erictraut may remember; I wasn't there for the discussion and I don't want to go too far into second-hand explanation past citing the PEP.
gvanrossum commentedon May 12, 2021
Testing this, mypy and pyright seem to disagree. I wrote this code:
Mypy accepts this, but pyright gives errors on the
hof(guard)and thehof2(notguard)calls:erictraut commentedon May 12, 2021
PEP 647 says:
I added this to the spec at your recommendation, Guido, because you argued (convincingly) that there were cases where we would want to distinguish between
boolandTypeGuard[T]in overloads.So it appears that mypy is not conforming to the PEP.
JelleZijlstra commentedon May 12, 2021
Thanks for the explanation! It's very limiting though if we can't safely change existing functions like
inspect.is*to use TypeGuards. Do you see a way to allow that?As a user I'd expect Guido's
hof(guard)example to succeed buthof2(notguard)to fail. TreatingTypeGuardas a subtype of bool would do that.gvanrossum commentedon May 12, 2021
JelleZijlstra commentedon May 12, 2021
I think you can, just tried this with mypy:
It does complain that the overloads overlap, but it infers the correct types.
jakebailey commentedon May 12, 2021
I tried to create some concrete examples to show this back in the "final call for comments" thread in relation to python/typing#253 (comment), and some seemed to think that this restriction was unnecessary, but I gave up thinking about it after getting berated enough to not want to continue participating in the thread anymore.
srittau commentedon May 12, 2021
If
TypeGuardis treated as a subtype ofbool, the overloads should work, as long as the more specific overload (TypeGuard) is listed first. We have quite a few cases in typeshed where we do that. (Not withTypeGuardobviously.)gvanrossum commentedon May 12, 2021
@jakebailey
What does "this" refer to? The restriction on overloads being distinct? Or something related to TypeGuard?
Sorry for your experience. We should do better.
7 remaining items
gvanrossum commentedon May 12, 2021
JelleZijlstra commentedon May 12, 2021
pyright doesn't actually support Guido's example, but it does infer
xto be of typeTypeGuard[]and prevent you from passingxto a function that takes abool.Stepping back, the reason I'm pushing for a change here is that it would be unfortunate if we can't take advantage of TypeGuard on any existing functions. Users who want narrowing from functions like
inspect.isfunctionwould have to use a copy of the function that returns a TypeGuard instead. That's not an outcome I'd like to see. Of course, I should have brought this up while the PEP was under discussion instead of now; sorry for missing it.I still think that making
TypeGuarda subclass of bool would work well in practice. There are some soundness problems, but those seem mostly theoretical. I have a hard time imagining the unsoundness manifesting as anything worse than a missed narrowing. I'm open to other solutions though.jakebailey commentedon May 12, 2021
TypeGuard[List[str]]is the type of the expression, and assigning it just uses that type. Transparently converting it to bool in specific circumstances sounds like it would cause oddities (as "getting the type" of this call is the same operation if you were to want to use it by passing it to something that accepts a type guard, like a potential filter function).Of course, if TypeGuard were compatible in some sense, then how this is chosen wouldn't really matter, except maybe that variables may be inferred to have a type that's more specific than what a user wants, potentially (but that's nothing new).
gvanrossum commentedon May 13, 2021
@jakebailey
I guess in our minds we think of this fundamentally differently. To me,
TypeGuard[List[str]]is special syntax that adds an attribute to the function outside the type system proper, while its return type is justbool.If you consider the TypeScript equivalent,
arg is SomeType, I'd say that clearly the return type is notarg is SomeType, the return type is boolean, and the special syntax makes the function special in certain contexts.In Python,
TypeGuard[...]is not a generic type, it just looks like one because that's pretty much the only syntax we have available.erictraut commentedon May 16, 2021
Just catching up on this thread...
I confirmed that there was a bug in pyright's implementation when evaluating the return type of a call to a user-defined type guard. It should evaluate the return type as
bool, not asTypeGuard[T]. That bug will be fixed in the next release.The question that is being raised here is whether a function that accepts a callback of type
Callable[..., bool]should accept a user-defined type guard function. I also see no reason why it shouldn't, since the runtime return type of such a function isbool. I've made the change in pyright to allow this, and it will be in the next release also.JelleZijlstra commentedon May 16, 2021
Thanks Eric, that's great to hear! Then I think the only thing blocking us from using TypeGuard in typeshed is PyCharm.
srittau commentedon May 16, 2021
I think PyCharm should mostly be considered a "soft blocker". While we shouldn't break central, often used functions, adding
TypeGuardto less often used modules shouldn't be a problem. The worst that PyCharm does is to warn about something it doesn't understand. As a PyCharm user I'm quite used to a fair amount of warnings, unfortunately.erictraut commentedon May 16, 2021
I just noticed that there's still a small difference between the behavior of mypy and pyright. Mypy apparently allows non-type-guard functions (those that return a
bool) to be passed to a function that indicates it requires a type guard function. This can be seen using Guido's test code above.@gvanrossum, can you explain why you think this should not be an error? Is there a compelling reason to support this? I think it creates some problems, specifically for overload matching. It also creates problems for generics used in TypeGuards. For example, what would you expect the return result to be for the following call?
Maybe it's OK for mypy and pyright to differ in this specific use case, but I want to make you all aware of this because pyright's current behavior (with my recent changes noted above) requires that any use of TypeGuard callbacks in typeshed must be done with overloads, and the TypeGuard overload must appear prior to the non-TypeGuard overload, like this:
improve inspect.pyi
gvanrossum commentedon May 17, 2021
I wasn’t expressing an opinion on what is correct, just observing a difference. I don’t think I will get to changing mypy even if it is decided it’s wrong, but someone else might. Sorry!
JelleZijlstra commentedon May 17, 2021
I agree that the mypy behavior Eric highlighted doesn't make sense, so I opened an issue about it. With Sebastian's comment on PyCharm, I think we're OK to start using TypeGuard in typeshed, so I'm closing this issue.
Thanks everyone for the work on TypeGuard!
jakebailey commentedon May 17, 2021
Bumped pyright's locked version to 1.1.140, which includes the aforementioned fixes.
Happy to know that the difference was a bug; I probably should have known better when I went to TS, wrote the same type guard, and the hover said
boolean, not something special... 🙂improve inspect.pyi (#5473)