I am using pyright in my project to have some type-safety and I stub upon a type problem that I don't understand. It is really easy to fix (using covariant=True
in the TypeVar) but I don't even understand why is it a problem itself.
Here is an example of my problem:
from typing import TypeVar, Callable, Generic
class A:
...
# Any subclass of A or A that is needed in a Generic
AT = TypeVar("AT", bound=A)
# Generic T where T is a subclass of A or A
class B(Generic[AT]):
...
# Decorator that converts a method of subclass of A (or A) to an instance of B[AT]
def to_b_instance(method: Callable[[AT], Any]) -> B[AT]:
return B()
# Decorator that takes a predicate B[AT]->bool and an instance of B[AT]
def check_b_instance(predicate: Callable[[B[AT]], bool]) -> Callable[[B[AT]], Any]:
def inner(b: B[AT]):
...
return inner
# A predicate that can check B[SubA]'s instances
def my_predicate(b: B["SubA"]):
return True
class SubA(A):
@check_b_instance(my_predicate) # <- The problem is displayed here (full error bellow)
@to_b_instance
def methode(self):
pass
Argument of type "B[Self@SubA]" cannot be assigned to parameter of type "B[SubA]"
"B[Self@SubA]" is incompatible with "B[SubA]"
TypeVar "AT@B" is invariant
Type "Self@SubA" cannot be assigned to type "SubA" Pyright(reportGeneralTypeIssues)
Firstly this error is not raised by mypy, making me confident that this is a bug on pyright side.
I know that I can fix the problem by changing the AT TypeVar TypeVar("AT", bound=A, covariant=True)
but I just don't understand how Self@SubA
is different than SubA
and what rule forces me to do so.
By the way, explicitly typing the self
parameter with SubA
seems to fix the problem too, without adding any additional error. I really don't get it.
# AT is not covariant
class SubA(A):
@check_b_instance(my_predicate)
@to_b_instance
def methode(self: "SubA"): # <- No error anymore
pass
(Co-/contra-/in-)variance is related to how generics and subclasses interact. In your specific case, Self@SubA
isn't necessarily SubA
iself, but might be any subclass of it. Your predicate, however, only expects its argument to be of type B[SubA]
. Covariance cannot be assumed by default - for instance, if B
was List
and SubA
was object
, my_predicate(b: List[object])
would've been allowed to call both b.append(123)
and b.append("Hello")
, since all of those are objects
. However, while int
is a subclass of object
, List[int]
can not be treated as a subclass of List[object]
for this regard, since b.append("Hello")
would be invalid for it.
I haven't met any particularly outstanding introduction into generics and variance, but you may check out the Wikipedia page or some other StackOverflow questions about it.
EDIT: (To address the question from the comment): Consider that I add the following code to yours:
class Foo(SubA):
pass
foo = Foo()
foo.methode()
Now the call to methode
receives foo
as its self
argument, whose type is Foo
, not SubA
. This is fine, because OOP allow a subclass to be used whereever the parent class is expected. However, now the Self
type, when (explicitly or implicitly) used in the type signature of methode
is resolved to Foo
for this particular call. This Self
is referred to by pyright as Self@SubA
, becuase it is bound to SubA
. But, once again, it is currently Foo
. Now, it passes through the code added by the decorators until the obtained value of type B[Foo]
is attempted to be passed to my_decorator
. B[Foo]
is not considered a subclass of B[SubA]
, unless B is covariant on its generic parameter. So the invocation is invalid due to an incomatible type.
You may argue that there are no subclasses to SubA
in your code, but neither are there any rules preventing those from being introduced, so the type checker cannot make the assumption that the only type matching Self@SubA
would be SubA
itself.
When you explicitly annotate methode
's self
argument to be SubA
, everything works because now self
is regarded as an instance of SubA
even when it belongs to a deeper subclass. This results in B[SubA]
being the type of the parameter passed to my_decorator
, which is the expected one, regardless of B
's variance.