microsoft / pyright

Static Type Checker for Python

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Strict mode Pyright complains about certain types requiring generic arguments, however, these types are not generics in Python 3.8

WSH032 opened this issue · comments

Describe the bug

Strict mode Pyright complains about certain types requiring generic arguments, however, these types are not generics in Python 3.8

At least for AbstractContextManager and Popen, there are such issues. Other standard library types may also have similar problems, but I haven't tested them.

Code or Screenshots

Code sample in pyright playground

from contextlib import AbstractContextManager
from subprocess import Popen


# Expected type arguments for generic class "AbstractContextManager"
class Bar(AbstractContextManager):
    pass

# Expected type arguments for generic class "Popen"
class Baz(Popen):
    pass

Providing generic arguments indeed make pyright happy, however, running this code in Python 3.8 will result in errors.

Code sample in pyright playground

from contextlib import AbstractContextManager
from subprocess import Popen


# TypeError: 'ABCMeta' object is not subscriptable
class Bar(AbstractContextManager["Bar"]):
    pass

# TypeError: 'type' object is not subscriptable
class Baz(Popen[str]):
    pass

Related issues

#2246 (comment)

Pyright is working as designed here, so I don't consider this a bug.

Pyright works from type information provided by the typeshed project. It indicates that AbstractContextManager and Popen are generic classes. If you think this is an error, you could file a bug report in the typeshed project.

If you're able to do so, consider upgrading to a version of python newer than 3.8, which is quite old at this point and will no longer be "in support" later this year.

If you are not yet able to move to a more up-to-date version of python, you will likely need to disable strict mode type checking or suppress individual errors.

Thank you for your quick response! 👍

The upgrade is not a problem for me personally, but I maintain some libraries that need to support 3.8, after all, 3.8 is still within its lifecycle.

Although it might make CI testing a bit awkward, I can accept that.

My typical solution for this issue as I also mostly work on older python is,

if TYPE_CHECKING:
  BazBase = Popen[str]
else:
  BazBase = Popen

class Baz(BazBase):
  ...

Thanks, @hmc-cs-mdrissi ! That's exactly what I did.

But unfortunately, it doesn't work for AbstractContextManager

Code sample in pyright playground

from contextlib import AbstractContextManager
from typing import TYPE_CHECKING, Any


if TYPE_CHECKING:
    _AbstractContextManagerType = AbstractContextManager["Bar"]
else:
    _AbstractContextManagerType = AbstractContextManager

# Argument to class must be a base class
# Base class type is unknown, obscuring type of derived class
class Bar(_AbstractContextManagerType):
    def __exit__(self, *_: Any):
        pass

# Type of "bar" is unknown
with Bar() as bar:
    reveal_type(bar)

Add : TypeAlias. That fixes the error.

from typing_extensions import TypeAlias

if TYPE_CHECKING:
    _AbstractContextManagerType: TypeAlias = AbstractContextManager["Bar"]
else:
    _AbstractContextManagerType = AbstractContextManager

Older pyright versions were a bit more lax here and didn't require TypeAlias and I forgot it.

edit: Hmm recursive part with "Bar" seems to still cause an issue (say AbstractContextManager[str]). With non-recursive base type using TypeAlias it works fine. That feels like bug?

Examples below where I got rid of TYPE_CHECKING as while needed for 3.8 is unrelated to the issue.

from typing import TypeAlias
from subprocess import Popen


BarBase: TypeAlias = Popen["Bar"]
class Bar(BarBase): # Type error unknown
    ...
from subprocess import Popen

class Bar(Popen["Bar"]): # No error
    ...
from typing import TypeAlias
from subprocess import Popen


BarBase: TypeAlias = Popen[str]
class Bar(BarBase): # No error
    ...

Popen["Bar"] should be wrong, the generic argument of Popen is constrained to AnyStr. It seems Pyright did not detect this error.

Perhaps deriving from AbstractContextManager was not the best approach.

I initially considered doing so, but it turned out to be unfeasible:

from contextlib import AbstractContextManager
from typing_extensions import Self


class Foo(AbstractContextManager[Self]):
    def __exit__(self, *_: object) -> None:
        pass

May be following would be better:

from contextlib import AbstractContextManager
from typing_extensions import Self, assert_type


class Foo:
    def __enter__(self) -> Self:
        return self

    def __exit__(self, *_: object) -> None:
        pass


class Bar(Foo):
    pass


def func(bar: "AbstractContextManager[Bar]") -> None:
    assert isinstance(bar, AbstractContextManager)
    with bar as b:
        assert_type(b, Bar)


func(Bar())