Narrow type parameters (rather than variables)

I would like to make a function which, given a variable x of type T makes an object of type A[T] and passes x to its constructor (and a few extensions on this). I understand that we can do “type narrowing” on variables, i.e., we can determine that x is of some narrower type Q with isinstance, or TypeGuard or TypeIs, and then a type checker will know that x is of type Q. However, it seems that if x is of type T and we check with isinstance (or TypeGuard or TypeIs) that x is of a subtype of T, that doesn’t impact what current type checkers think about type T.

class A[T]:
    def __init__(self, x:T):
        self.x=x

class B(A[int]):
    pass
        
def failing_1[T](x:T) -> A[T] | None:
    if isinstance(x,int):
        return A[T](x) # Argument 1 to "A" has incompatible type "int"; expected "T" 
    return A[T](x)

def failing_2[T](x:T) -> A[T]:
    if isinstance(x,int):
        return B(x) # Incompatible return value type (got "B", expected "A[T]")
    return A[T](x)    

def working[T](x:T) -> A[T]:
    return A[T](x)

In the above, in failing_1, mypy sees the isinstance, and concludes that x is of type int. mypy forgets that x is of type T too, and what is clearly allows gives a mypy error. The function “working” illustrates that without the isinstance there is no confusion and the same code, applicable on the same input, works well.

In fact, I would also like to do something like failing_2, i.e., if we know that x is of type int (hence T=int) and that B is a subclass of A[int], then returning an object of type B where A[T]=A[int] is expected should be allowed. I tried to define explicitly T as covariant, but that doesn’t help.

The same problems arises when running pyright rather than mypy. (hence this is not likely to be a bug)

More generally, this kind of problem arises in several forms when I want to tell the type checker that two objects have related type parameters, e.g., as here (T vs A[T]), but also (x:T1 and y:T2 with T1 = A[T2])

Hence my question: do I miss some existing typing mechanism or does the current type system not allow to narrow type parameters (rather than just variables) ?

For failing_1, this was implemented yesterday actually, in mypy#19183. The second case is still unsafe, because T could be a subclass of int. Here’s an example:

from typing import Final

class SubInt(int):
    def child_behaviour(self) -> str: ...

class A[T]:
    def __init__(self, value: T):
        self.value: Final[T] = value  # Makes this covariant
        
    def get(self) -> T:
        return self.value

class B(A[int]):
    def __init__(self) -> None:
        super().__init__(42)
        
    def get(self) -> int:
        return 12

def failing_2[T](x: T) -> A[T]:
    if isinstance(x, int):
        return B()  # Unsafe!
    return A[T](x)

sub_a: A[SubInt] = failing_2(SubInt(12))
sub_a.get().child_behaviour()  # Boom!

Because B specifies int specifically, it’s free to create regular ints and return them where T was. But the T typevar in failing_2 requires the exact subclass to be returned.

Thanks for your nice answer.
Still, it leads to another question: you say:

failing_2 requires the exact subclass to be returned.

Indeed, but can I specify that a function can return any object of type A[T] or a subclass?

def failing_2[T,T2](x:T) -> T2: # T2 is bounded by T
    ...

I considered once:

def failing_2[T,T2:T](x:T) -> T2: 
    ...

but documentation explicitly says this kind of imposing relations between type variables is not allowed.