Typing to mark return values of current class type or any of its subclasses

Question:

I want to make sure that the from_dict in the following method works well in its subclasses as well. Currently, its typing does not work (mypy error "Incompatible return value type"). I think because the subclass is returning an instance of the subclass and not an instance of the super class.

from __future__ import annotations

from abc import ABC
from dataclasses import dataclass
from typing import ClassVar, Type


@dataclass
class Statement(ABC):
    @classmethod
    def from_dict(cls) -> Statement:
        return cls()


@dataclass
class Parent(ABC):
    SIGNATURE_CLS: ClassVar[Type[Statement]]

    def say(self) -> Statement:
        # Initialize a Statement through a from_dict classmethod
        return self.SIGNATURE_CLS.from_dict()


@dataclass
class ChildStatement(Statement):
    pass


@dataclass
class Child(Parent, ABC):
    SIGNATURE_CLS = ChildStatement

    def say(self) -> ChildStatement:
        # Initialize a ChildStatement through a from_dict classmethod
        # that ChildStatement inherits from Statement
        return self.SIGNATURE_CLS.from_dict()

The code above yields this MyPy error:

Incompatible return value type (got "Statement", expected "ChildStatement")  [return-value]

I think this is a use case for TypeVar in Statement but I am not sure how to implement and – especially – what the meaning behind it is.

Asked By: Bram Vanroy

||

Answers:

Subclass instance is an instance of its super class, according to typing rules. The error you are seeing is because from_dict is typed to be returning a Statement, and you are trying to return that value from say, which is guaranteed to return ChildStatement. So your problem is that you could potentially return more generic Statement where more specific ChildStatement is expected.

You need to somehow ensure MyPy that:

  1. from_dict returns its actual class, not generic Statement.
  2. SIGNATURE_CLS of Child will be ChildStatement and not possibly Statement (just assigning ChildStatement is not enough as you explicitly typed it as ClassVar[Type[Statement]]

You can do that with following pieces of code:

  1. use typing.Self to reflect return cls()
from typing import Self

@dataclass
class Statement(ABC):
    @classmethod
    def from_dict(cls) -> Self:
        return cls()

(It’s important to note that typing.Self is a very fresh addition introduced in Python 3.11, you might also need to update your MyPy to be able to take this into account)

  1. Explicitly type Child.SIGNATURE_CLS
@dataclass
class Child(Parent, ABC):
    SIGNATURE_CLS : ClassVar[Type[ChildStatement]] = ChildStatement

As a final note, I wonder about the sensibility of Statement.from_dict() method, as it isn’t any different than just using the standard constructor of Statement().

Answered By: matszwecja