basedpyright icon indicating copy to clipboard operation
basedpyright copied to clipboard

instance of class with a covariant generic can unsafely be narrowed to a subclass with the same generic type

Open DetachHead opened this issue 4 months ago • 3 comments

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

DetachHead avatar Oct 25 '25 10:10 DetachHead

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

beauxq avatar Oct 25 '25 13:10 beauxq

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.

beauxq avatar Oct 25 '25 13:10 beauxq

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

DetachHead avatar Oct 25 '25 14:10 DetachHead