Narrow type parameters (rather than variables) (original) (raw)
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) ?
TeamSpen210 (Spencer Brown) June 5, 2025, 2:55am 2
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.
jrp (Jan) June 5, 2025, 4:38am 3
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.