Allow for covariance in nested return types when subclassing

Viewed 314

Say we have the following example (it took me a while to think of a minimal example of my issue, sorry if real life context is not the best, but I thought I'd be better than just using names like Base and Child)

from typing import *
from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: float


class Store:
    def __init__(self, products: List[Product]):
        self.products: List[Product] = products

    def find_by_name(self, name) -> List[Product]:
        return [product for product in self.products if product.name is name]

I have defined the return type for my find_by_name method so when I'm coding I can know what the return type will be and operate based on that.

Now imagine I create a subclass of my Product and my Store (here it is rather useless but it can be really necessary, of course).

class Fruit(Product):
    color: str

class FruitStore(Store):
    def __init__(self, fruits: List[Fruit]):
        super().__init__(products=fruits)

    def find_by_name_and_color(self, name, color) -> List[Fruit]:
        return [fruit for fruit in self.find_by_name(name) if (fruit.name is name and fruit.color is color)]
        # Expected List[Fruit], got List[Product] instead

As commented out, PyCharm (and any annotation check) will detect that the return type for this function does not match the content given based on the return type of the function where content came from.

For readability purposes and easier debugging, I tried to replace the annotation with no luck:

    def find_by_name(self, name) -> List[Fruit]: return super().find_by_name(name)
        # Expected List[Fruit], got List[Product] instead

Not even replacing the whole function would suffice:

    def find_by_name(self, name) -> List[Fruit]:
        return [product for product in self.products if product.name is name]
        # Expected List[Fruit], got List[Product] instead

I have to replace the variable definition in init:

    def __init__(self, fruits: List[Fruit]):
        self.products: List[Fruit] = fruits

Which in turn, means replacing the whole class, and making inheritance useless. How to replace only annotations and return types without having to replace the whole code?

Edit: including the terminology introduced in the comment box (which I was not aware of), I think my question would rephrase to: when using methods with broad types from parent classes inside a child's method with a narrower return type, how to allow for contravariance?

Second thought I think that varying the default broad type from parent function to a standard narrower type would be a better practice than allowing broader return types.

Ugly diagram:

________________________________________________________________________
Input                        |  Inner            | Output
------------------------------------------------------------------------
                             |
(Parent.method -> broadType) |-> (Child.method2 -> narrowedType)
                             |
------------------------------------------------------------------------
1 Answers

I think I have found a workaroud to my problem. First we create a type:

ProductType = TypeVar('ProductType', bound=Product, covariant=True)

(Name could be better, maybe a data scructure that would be type.Product).

Now we implement it:

class Store:
    def __init__(self, products: List[ProductType]):
        self.products: List[ProductType] = products

    def find_by_name(self, name) -> List[ProductType]:
        return [product for product in self.products if product.name is name]

Turns out leaving Fruit iniherting from Product works OK, so from experience I say that TypeVar instance should be used when annotating, not when subclassing.

class Fruit(Product):
    color: str

And lastly code just works. Class FruitStore is retreiving return types appropriately. There is no need replacing anything. This is because covariant types allow to expect subtypes of the defined bound where the latter is expected.

Related