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