1

Considering the following structure of classes:

from abc import ABC, abstractmethod


class ModelSettings(ABC):
    ...

class BlueModelSettings(ModelSettings):
    ...

class RedModelSettings(ModelSettings):
    ...


class Model(ABC):
    @abstractmethod
    def compute(self, settings: ModelSettings):
        ...

class BlueModel(Model):
    def compute(self, settings: BlueModelSettings):
        ...

class RedModel(Model):
    def compute(self, settings: RedModelSettings):
        ...

MyPy is complaining that the implementations of compute in BlueModel and RedModel violate the Liskov substitution principle. I understand this and also understand why it makes sense.

My question is, instead of the above structure, what would be another approach that would satisfy the following requirements:

  • base model has a contract that compute should receive settings, not apples or oranges
  • but each specific model should accept only settings relevant to its own model kind

In a way, I essentially want that a subclass' method argument is stricter than what its base class stipulates, which goes directly against the Liskov principle. Which is what's telling me there might be a more suitable approach.

Note:

  • Model is library code so it can't know of e.g. BlueModelSettings which is client code
Jivan
  • 21,522
  • 15
  • 80
  • 131

1 Answers1

2

You need your Model to be generic in the type of settings.

T = TypeVar('T', bound=ModelSettings)

class Model(ABC, Generic[T]):
    @abstractmethod
    def compute(self, settings: T):
        ...

class BlueModel(Model[BlueModelSettings]):
    def compute(self, settings: BlueModelSettings):
        ...

class RedModel(Model[RedModelSettings]):
    def compute(self, settings: RedModelSettings):
        ...
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Impressive. So to be completely square with the condition on `Settings`, we would then declare `T = TypeVar('T', bound=Settings)`, right? – Jivan Jun 20 '22 at 15:16
  • 1
    Yeah. (I submitted the answer prematurely). Setting the bound ensures you can't create a subclass or instantiate `Model` with something that isn't a correct subclass of `ModelSettings`). – chepner Jun 20 '22 at 15:19
  • It doesn't remove anything to this answer, but it seems that MyPy allows instantiating `BlueModel(Model)` with no type parameter on `Model` and in this case doesn't check whether `BlueModelSettings` is a subclass of `ModelSettings` — are you aware of a MyPy setting that checks for the type parameter presence (currently it complains if it's wrong, but doesn't complain if absent) – Jivan Jun 20 '22 at 15:37
  • *mypy allows `implementing`, not instantiating (typo) – Jivan Jun 20 '22 at 16:37