Home > OS >  How can I enforce overridden methods to return a specific subset of types?
How can I enforce overridden methods to return a specific subset of types?

Time:10-27

I'm defining a simple class hierarchy for metrics of objects in images. For example, one metric is 'area', the number of pixels. Another might be 'color', e.g. a string like 'green' or 'orange'. The base class BaseMetric only has an abstract method compute for computing and returning the metric's value, while the subclasses actually implement their respective metric computation logic.

I'm trying to enforce the return type of compute in a given child class to be either int or str (not Union[int, str], only one or the other). Using Generics feels like the way to go, however it seems my editor and interpreter don't mind if the subclass changes the return type to be anything else. For example, my code doesn't fail when I define a subclass that inherits from BaseMetric[int] but whose compute function returns bool. How can I achieve this sort of type enforcement?

I'm using VS Code with Pylance for static type checking. And I'm using the overrides module to help enforce subclass typing at both static time and runtime. The code below runs without error, but I wanted it to fail:

from abc import ABC, abstractmethod
from typing import Generic, TypeVar

from overrides import overrides

T = TypeVar("T", int, str)

class BaseMetric(ABC, Generic[T]):
    @abstractmethod
    def compute(self) -> T:
        raise NotImplementedError()

class AreaMetric(BaseMetric[int]):
    @overrides
    def compute(self) -> int:
        return 100

class ColorMetric(BaseMetric[str]):
    @overrides
    def compute(self) -> str:
        return "orange"

# should fail because return type bool is different
# from generic class type int, but doesn't fail
class InvalidMetric(BaseMetric[int]):
    @overrides
    def compute(self) -> bool:
        return True

AreaMetric().compute()
ColorMetric().compute()
InvalidMetric().compute()

CodePudding user response:

I am not familiar with the overrides package and from reading the Readme in the repo you linked I cannot tell, if they promise handling generics properly.

Either way, the reason why static type checkers have no problem with that return annotation of InvalidMetric.compute, is very simple: bool is a subclass of int.

You can easily verify that with issubclass(bool, int).

Type safety for overriding inherited methods generally dictates that return types are covariant (while arguments are contravariant). That is, if the parent class has a method returning type T, it is safe for that method on the child class to return S, as long as S is a subtype of T.

Since you constrained your generic type variable T to be either str or int and you specified your generic superclass for InvalidMetric to be BaseMetric[int], the inherited method compute is constrained to return int. Thus, it is perfectly fine to override it to return bool because that is a subtype of int (at least in Python).

What would be wrong and actually probably should trigger an error with your overrides, is if we were to annotate the return type with something that isn't a subtype of int, like object for instance:

...
class InvalidMetric(BaseMetric[int]):
    @overrides
    def compute(self) -> object:
        return True
...

This causes mypy to rightfully complain:

error: Return type "object" of "compute" incompatible with return type "int" in supertype "BaseMetric"  [override]

However, it seems this does nothing to trigger an error at runtime. As I said, I don't know the contract of overrides, so this may or may not be a bug for their issue tracker.


Just as a side note: You mentioned your desire to "enforce" this or that type, but don't forget that this is never going to happen by just adding type annotations. Python is dynamically typed. By default, the interpreter won't care at all about any types you mentioned before. I wasn't sure from the way you phrased it, if you were fully aware of this, so I just wanted to mention it.

  • Related