Search code examples
pythonpython-typingpyright

Python : pyright : Self@T is not compatible with T


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

Solution

  • (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.