Skip to content

PEP 647 (TypeGuard) tracker #5406

Closed
Closed

Description

@JelleZijlstra

I'd like to start using TypeGuard in typeshed. Here's what we need:

Activity

  1. JelleZijlstra commented on May 11, 2021

    @JelleZijlstra
    MemberAuthor

    @sproshev does PyCharm support TypeGuard already?

  2. sproshev commented on May 11, 2021

    @sproshev
    Contributor

    Not even planned :( It is going to be released in Python 3.10, right?

  3. JelleZijlstra commented on May 11, 2021

    @JelleZijlstra
    MemberAuthor

    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.

  4. JelleZijlstra commented on May 12, 2021

    @JelleZijlstra
    MemberAuthor

    Some examples of functions where this could be useful:

    • builtins.callable should return TypeGuard[Callable[..., Any]]
    • Various functions in inspect like inspect.isfunction, isclass, etc.
  5. jakebailey commented on May 12, 2021

    @jakebailey
    Contributor

    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?

  6. hauntsaninja commented on May 12, 2021

    @hauntsaninja
    Collaborator

    (Why does the PEP say so?)

  7. jakebailey commented on May 12, 2021

    @jakebailey
    Contributor

    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.

  8. gvanrossum commented on May 12, 2021

    @gvanrossum
    Member

    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)
    
  9. erictraut commented on May 12, 2021

    @erictraut
    Contributor

    PEP 647 says:

    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.

  10. JelleZijlstra commented on May 12, 2021

    @JelleZijlstra
    MemberAuthor

    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.

  11. gvanrossum commented on May 12, 2021

    @gvanrossum
    Member
  12. JelleZijlstra commented on May 12, 2021

    @JelleZijlstra
    MemberAuthor

    I think you can, just tried this with mypy:

    from typing import Any, Callable, overload
    
    @overload
    def f(x: Callable[[], bool]) -> str: ...
    @overload
    def f(x: Callable[[], int]) -> float: ...
    def f(x: Any) -> Any: ...
    
    def b() -> bool: return True
    def i() -> int: return 0
    
    reveal_type(f(b))  # Revealed type is 'builtins.str'
    reveal_type(f(i))  # Revealed type is 'builtins.float'

    It does complain that the overloads overlap, but it infers the correct types.

  13. jakebailey commented on May 12, 2021

    @jakebailey
    Contributor

    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.

  14. srittau commented on May 12, 2021

    @srittau
    Collaborator

    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.)

  15. gvanrossum commented on May 12, 2021

    @gvanrossum
    Member

    @jakebailey

    I tried to create some concrete examples to show this

    What does "this" refer to? The restriction on overloads being distinct? Or something related to TypeGuard?

    after getting berated enough to not want to continue participating

    Sorry for your experience. We should do better.

  16. 7 remaining items

  17. gvanrossum commented on May 12, 2021

    @gvanrossum
    Member
  18. JelleZijlstra commented on May 12, 2021

    @JelleZijlstra
    MemberAuthor

    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.

  19. jakebailey commented on May 12, 2021

    @jakebailey
    Contributor

    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).

  20. gvanrossum commented on May 13, 2021

    @gvanrossum
    Member

    @jakebailey

    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.

  21. erictraut commented on May 16, 2021

    @erictraut
    Contributor

    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 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.

  22. JelleZijlstra commented on May 16, 2021

    @JelleZijlstra
    MemberAuthor

    Thanks Eric, that's great to hear! Then I think the only thing blocking us from using TypeGuard in typeshed is PyCharm.

  23. srittau commented on May 16, 2021

    @srittau
    Collaborator

    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.

  24. erictraut commented on May 16, 2021

    @erictraut
    Contributor

    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.

    def notguard(x: Any) -> bool: ...
    
    def hof2(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?

    def my_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:

    @overload
    def my_filter(func: Callable[[Any], TypeGuard[T]]) -> List[T]: ...
    @overload
    def my_filter(func: Callable[[Any], bool) -> List[Any]: ...
  25. added a commit that references this issue on May 16, 2021
  26. gvanrossum commented on May 17, 2021

    @gvanrossum
    Member

    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!

  27. JelleZijlstra commented on May 17, 2021

    @JelleZijlstra
    MemberAuthor

    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!

  28. jakebailey commented on May 17, 2021

    @jakebailey
    Contributor

    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... 🙂

  29. added a commit that references this issue on May 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    project: feature trackerTracks whether a typing feature can be used in typeshed stubs

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions