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

No way to avoid an error when using isinstance with a generic class in strict mode #2128

Closed
BurningLights opened this issue Dec 1, 2021 · 7 comments
Labels
enhancement New feature or request fixed in next version (main) A fix has been implemented and will appear in an upcoming version

Comments

@BurningLights
Copy link

Environment data

  • Language Server version: v2021.11.2
  • OS and version: Windows 10 Build 19042.1348
  • Python version (& distribution if applicable, e.g. Anaconda): Python 3.9.5

Expected behaviour

There needs to be some way in strict mode to have an isinstance check with a generic class without getting an error. I suggest defaulting type arguments to Any after an isinstance check, rather than Unknown.

Actual behaviour

For a contrived example:

def test(var: Any):
    if isinstance(var, dict):
        print(var.keys())

In strict mode, the var.keys() produces the type error

Argument type is partially unknown
Argument corresponds to parameter "values" in function "print"
Argument type is "_dict_keys[Unknown, Unknown]"

And adding generic parameters to the isinstance check (which I know is not how it's supposed to work)

def test(var: Any):
    if isinstance(var, dict[str, Any]):
        print(var.keys())

There's no error at var.keys(), but the isinstance check produces the type error

Second argument to "isinstance" must be a class or tuple of classes
  TypeVar or generic type with type arguments not allowed
@github-actions github-actions bot added the triage label Dec 1, 2021
@erictraut
Copy link
Contributor

Yeah, it's unfortunate that isinstance doesn't accept specialized classes like dict[str, Any].

In practice, this isn't typically an issue because most people who use strict type checking try to avoid using Any. If you know var is a union of possible types (including specialized types), pyright will narrow the union accordingly. For example:

def test(var: int | str | dict[str, str]):
    if isinstance(var, dict):
        reveal_type(var) # dict[str, str]

One possible (but admittedly ugly) workaround here is:

        d = cast(dict[Any, Any], var)
        print(d.keys())

If you don't like that, you could use a # type: ignore to suppress the error on that line.

@debonte debonte added the waiting for user response Requires more information from user label Dec 1, 2021
@github-actions github-actions bot removed the triage label Dec 1, 2021
@BurningLights
Copy link
Author

I guess I should have picked a better example. I used the annotation Any as a contrived example. Here's a more specific example demonstrating my exact issue, using Django 3.2.6 and django-stubs 1.9.0:

from typing import TypeVar
from django.db.models import QuerySet
_T = TypeVar('_T', bound=Model, covariant=True)

class TestQuerySet(QuerySet[_T]):
    pass

def test(var: QuerySet[Any]):
    if isinstance(var, TestQuerySet):
        print(var.get())

It doesn't seem to propagate the generic parameter within the isinstance block because the print(var.get()) says that the return value is Unknown, rather than Any, as expected.

@erictraut
Copy link
Contributor

Thanks for the additional context.

I agree that the type argument should be preserved here. The isinstance type narrowing logic already attempts to retain the type argument, but it failed to do so when the type parameter was bound, as in your example. I've fixed this issue, so it now works with bound and constrained TypeVars. This will be included in the next release.

@erictraut erictraut added enhancement New feature or request fixed in next version (main) A fix has been implemented and will appear in an upcoming version and removed waiting for user response Requires more information from user labels Dec 30, 2021
@bschnurr
Copy link
Member

bschnurr commented Jan 6, 2022

This issue has been fixed in version 2022.1.0, which we've just released. You can find the changelog here: CHANGELOG.md

@mheripsos
Copy link

mheripsos commented Sep 18, 2024

I'm not sure how to get the version number from the VSCode extension, but I am on the pre-release version with auto-update, so I should be up to date.

I am seeing the following behavior which sounds a lot like the issue in this ticket:

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

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
        else:
            print(other.other)

I've put in the inferred types by pylance in the comments above. Specifically in the if block, we see that other is inferred to be Child[Unknown] even when outside the block it has already inferred it to be Parent[T, U].

Regardless of even the other type, we should know that in any case where other: Parent[T, U], then other.child: T regardless of any typeguards.

Shouldn't the type be inferred to Child[T] at this point since it would be the only intersection of Parent[T, U] and Child. There might be some slip in my logic that I am missing.

Update:
This additional case may help diagnose:

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)

The Sibling class is correctly inferred. The only difference being that the T and U of Sibling are independent.

@heejaechang
Copy link
Contributor

@mheripsos can you open a new issue rather than adding to closed issue, so people can pick it up?

@mheripsos
Copy link

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)

Submitted (#6515 )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request fixed in next version (main) A fix has been implemented and will appear in an upcoming version
Projects
None yet
Development

No branches or pull requests

6 participants