instance of class with a covariant generic can unsafely be narrowed to a subclass with the same generic type
Description
Code sample in basedpyright playground
from typing import reveal_type
class Box[T]:
def __init__(self, value: T, /) -> None:
self._value: T = value
def get(self) -> T:
return self._value
class Banana[U](Box[U]):
def put(self, new: U, /) -> None:
self._value: U = new
def f(x: Box[int | str]) -> None:
if isinstance(x, Banana):
# x incorrectly narrowed to `Banana[int | str]`
reveal_type(x)
x.put(42)
strbanan = Banana[str]("(")
f(strbanan) # !!!
reveal_type(strbanan.get()) # str, but int at runtime
related: #1444
a simpler example:
from collections.abc import Sequence
def foo(x: Sequence[int]) -> None:
if isinstance(x, list):
x.append(3)
y: list[bool] = [True, False]
foo(y)
# list[bool] now has `3` in it
I think it's not really that closely related to 1444
An option to consider - I'm not sure which way is best:
Instead of narrowing to list[Unknown]
we could keep the list[int] but report a new diagnostic on the narrowing line.
def foo(x: Sequence[int]) -> None:
if isinstance(x, list): # warning: narrowing from covariant to invariant
...
I'm thinking about the amount of effort required to ignore the error.
list[Unknown] might have to be ignored in a bunch of places and require more effort.
The new diagnostic could just have a # pyright: ignore on the one line.
i think i'd prefer to have it narrow correctly even though it would make the error more difficult to suppress. the behavior can be added to the strictGenericNarrowing setting so users can disable it if they want