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.