Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

isinstance Does not Narrow Generic Parameters #9141

Open
mheripsos opened this issue Oct 3, 2024 · 3 comments
Open

isinstance Does not Narrow Generic Parameters #9141

mheripsos opened this issue Oct 3, 2024 · 3 comments
Labels
addressed in next version Issue is fixed and will appear in next published version enhancement request New feature or request

Comments

@mheripsos
Copy link

mheripsos commented Oct 3, 2024

Directed to new issue from microsoft/pylance-release#2128

Environment data

  • Pylance version: v2024.9.103
  • OS and version: Windows 11 Pro 23H2 build 22631.4169
  • Python version (& distribution if applicable, e.g. Anaconda): 3.12.4

Code Snippet

class Parent[T, U]:
    def __init__(self, value: T, other: U) -> None:
        self.value = value
        self.other = other

class Sibling[T, U](Parent[T, U]):
    ...

class Child[T](Parent[T, T]):
    def meth[U](self, other: Parent[T, U]) -> None:
        print(other.value) # other: Parent[T@Child, U@meth]; .value: T@Child
        if isinstance(other, Child):
            print(other.value) # other: Child[Unknown]; .value: Unknown
        elif isinstance(other, Sibling):
            print(other.value) # other: Sibling[T@Child, U@meth]; .value: T@Child
        else:
            print(other.other)

Repro Steps

  1. Just let Pylance parse the above code

Expected behavior

in the if isinstance(other, Child) the type of other should be narrowed to Child[T] / Parent[T, T] and U should be infered to T.

From my understanding, unless there is some slip in my logic I'm not catching, this is the only valid inference that matches both other: Parent[T, U] and other: Child[Unknown]

Actual behavior

Inferences left inline above in the comments, I wrote out Pylance's inferences.

Particularly, again in that if block, other: Child[Unknown] instead of other: Child[T]

Logs

The "Python" log looks uninteresting, and the "Python Language Server" log blew up with a ton of stuff. Not sure what I would be looking for in here, but I think it's a bad idea to just dump it in here.

@rchiodo
Copy link
Collaborator

rchiodo commented Oct 3, 2024

Typechecking issue, transferring to Pyright.

@erictraut erictraut added the enhancement request New feature or request label Oct 3, 2024
@mheripsos
Copy link
Author

I Initially marked this as "Bug" because the inference is correct for Sibling class but not for Child. That distinction appears to be a bug, but I guess it could be considered an enhancement as well if the Sibling case works only incidentally.

erictraut added a commit that referenced this issue Oct 3, 2024
…es where the filter type (the second argument) is a child of the un-narrowed type and the child has a type parameter that is used multiple times in its base class (e.g. `Child[T](Parent[T, T])`). This addresses #9141.
erictraut added a commit that referenced this issue Oct 3, 2024
…es where the filter type (the second argument) is a child of the un-narrowed type and the child has a type parameter that is used multiple times in its base class (e.g. `Child[T](Parent[T, T])`). This addresses #9141. (#9144)
@erictraut
Copy link
Collaborator

Pyright does try to retain type arguments when an isinstance type guard is used. You can see that it does so in the Sibling case, although it doesn't in the Child case. By comparison, mypy (another popular Python type checker) doesn't preserve type arguments in either case.

class Parent[T, U]:
    def __init__(self, value: T, other: U) -> None: ...

class Sibling[T, U](Parent[T, U]): ...

class Child[T](Parent[T, T]):
    def meth[U](self, other: Parent[T, U]) -> None:
        if isinstance(other, Sibling):
            reveal_type(other) # Pyright reveals Sibling[T, U], mypy reveals Sibling[Any, Any]

        if isinstance(other, Child):
            reveal_type(other) # Pyright reveals Child[Unknown], mypy reveals Child[Any]

The challenge with the Child isinstance check is that the un-narrowed type of other is Parent[T, U]. The question is whether a type checker can identify a valid type X such that Child[X] is a subtype of Parent[T, U] given that Child[X] derives from Parent[X, X]. This is tricky because of variance rules. In your example, there is a valid solution for X: T | U, so pyright could safely narrow its type to Child[T | U]. Pyright's algorithm is not currently sophisticated enough to handle this case, so it falls back to Unknown.

I've enhanced the algorithm so it can handle this condition. This will be included in the next release.

@erictraut erictraut added the addressed in next version Issue is fixed and will appear in next published version label Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addressed in next version Issue is fixed and will appear in next published version enhancement request New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants