This brings the most succinct form that prevents the type checker from seeing it all in the same scope, and every scope being individually fine other than the TypeIs function to:
from typing_extensions import TypeVar, TypeIs
from typing import Generic
X = TypeVar("X", str, int, str | int, covariant=True, default=str | int)
class A(Generic[X]):
def __init__(self, i: X, /):
self._i: X = i
@property
def i(self) -> X:
return self._i
class B(A[X], Generic[X]):
def __init__(self, i: X, j: X, /):
super().__init__(i)
self._j: X = j
@property
def j(self) -> X:
return self._j
def boom(x: A[int]) -> int:
if isinstance(x, B):
print(x, x.i, x.j)
return x.i + x.j
return x.i
def bad(x: A) -> TypeIs[A[int]]:
return isinstance(x.i, int)
def indirection(x: A):
if bad(x):
boom(x)
def example():
b: B[int | str] = B(1, "this")
indirection(b)
Adding the detail that It’s the same type var in both parent and subclass closes the possibility that it’s just bad intersection narrowing that no type checker handles right now.
While this example is slightly more contrived, the one mirroring numpy’s ndarray structure is not, this one is just shorter to be easier to follow and reason about, the numpy-like one remains above to help show why this can come up in real code at some point, rather than be a hypothetical.
I’ll be trying to reduce my other examples for other issues in the PR to be similarly short, I’ll refrain from further followups on this till the PR is ready unless there’s still further detractions unaccounted for on this one.