I'm getting type checker errors on the following code, I would love to understand how to resolve the error.
The following base class has an abstract class method, I want that every child class that inherits from it will implement a decode
function that returns an instance of the child class.
from abc import ABC, abstractmethod
from typing import TypeVar
TMetricBase = TypeVar("TMetricBase", bound="MetricBase")
class MetricBase(ABC):
@abstractmethod
def add(self, element: str) -> None:
pass # pragma: no cover
@classmethod
@abstractmethod
def decode(cls, json_str: str) -> TMetricBase:
pass # pragma: no cover
Child class looks like the following
import json
from typing import Any, Callable, List, Mapping, Optional
from something import MetricBase, TMetricBase
class DiscreteHistogramMetric(MetricBase):
def __init__(self, histogram: Optional[Mapping[str, int]]) -> None:
super().__init__()
self._histogram = dict(histogram) if histogram else {}
def add(self, element: str) -> None:
self._histogram[element] = self._histogram.get(element, 0) + 1
@classmethod
def decode(cls, json_str: str) -> "DiscreteHistogramMetric":
json_obj = json.loads(json_str)
histogram_map = json_obj["DiscreteHistogramMetric"]
return cls(histogram=histogram_map)
I'm getting the following error:
error: Return type of "decode" incompatible with supertype "MetricBase"
When changing decode
's return type to TMetricBase
, I get the following error:
error: Incompatible return value type (got "DiscreteHistogramMetric", expected "TMetricBase")
Abstract classes cannot be instantiated, but they can be subclassed. When an abstract class is subclassed, the subclass usually provides implementations for all of the abstract methods in its parent class. However, if it does not, then the subclass must also be declared abstract .
Here's how you can add type hints to our function: Add a colon and a data type after each function parameter. Add an arrow ( -> ) and a data type after the function to specify the return data type.
Typing defines a standard notation for Python function and variable type annotations. The notation can be used for documenting code in a concise, standard format, and it has been designed to also be used by static and runtime type checkers, static analyzers, IDEs and other tools.
Introduction to Python type hints It means that you need to declare types of variables, parameters, and return values of a function upfront. The predefined types allow the compilers to check the code before compiling and running the program.
We cannot create an instance of an abstract class. An abstract class typically includes one or more abstract methods or property declarations. The class which extends the abstract class must define all the abstract methods. The following abstract class declares one abstract method find and also includes a normal method display.
When a string-based type hint is acceptable, the __qualname__ item can also be used. It holds the name of the class, and it is available in the body of the class definition. class MyClass: @classmethod def make_new (cls) -> __qualname__: return cls () By doing this, renaming the class does not imply modifying the type hints.
The following abstract class declares one abstract method find and also includes a normal method display. In the above example, Person is an abstract class which includes one property and two methods, one of which is declared as abstract. The find () method is an abstract method and so must be defined in the derived class.
Note that None as a type hint is a special case and is replaced by type (None). Use the NewType helper class to create distinct types: The static type checker will treat the new type as if it were a subclass of the original type. This is useful in helping catch logical errors:
The error has to do with how you're having just a single TypeVar in the return type of decode
. It's unclear what that would mean, exactly -- you're more or less trying to declare that every single subclass of MetricBase
needs to support returning any other arbitrary subclass of MetricBase
, which it'll somehow magically infer based on how that function is being called.
This isn't really something that's possible to do in Python.
What you'll need to do instead is one of the following:
MetricBase
a generic class and have your subclasses inherit a parameterized version of MetricBase
.TMetricBase
in the decode
parameters in some way. (This way, we can actually deduce what the return type ought to be).I'm assuming you've already considered the first solution and rejected it: it would make our program type check, but would also make the decode
method somewhat useless/require some clunky casting.
The second solution looks something like this:
from abc import ABC, abstractmethod
from typing import TypeVar, Generic
TMetricBase = TypeVar("TMetricBase", bound="MetricBase")
class MetricBase(ABC, Generic[TMetricBase]):
@classmethod
@abstractmethod
def decode(cls, json_str: str) -> TMetricBase:
pass
class DiscreteHistogramMetric(MetricBase['DiscreteHistogramMetric']):
@classmethod
def decode(cls, json_str: str) -> "DiscreteHistogramMetric":
pass
By having DiscreteHistogramMetric
subclass MetricBase[DiscreteHistogramMetric]
instead of just MetricBase
directly, we can actually constrain the typevar to something meaningful.
This solution is still a little clunky though -- having to subclass MetricBase
requires us to start using generics wherever we use MetricBase which is pretty annoying.
The third solution on the surface initially sounds even clunkier: are we going to add in some extra dummy third param or some nonsense? But it turns out there's a nice trick we can use -- we can use generic selfs to annotate the cls
variable!
Normally, the type of that variable is inferred and doesn't need to be annotated, but in this case, it's helpful to do so: we can use information about what exactly cls
is to help produce a more refined return type.
Here's what it looks like:
from abc import ABC, abstractmethod
from typing import TypeVar, Type
TMetricBase = TypeVar("TMetricBase", bound="MetricBase")
class MetricBase(ABC):
@classmethod
@abstractmethod
def decode(cls: Type[TMetricBase], json_str: str) -> TMetricBase:
pass
class DiscreteHistogramMetric(MetricBase):
def __init__(self, something: str) -> None:
pass
@classmethod
def decode(cls: Type[TMetricBase], json_str: str) -> TMetricBase:
# Note that we need to use create the class by using `cls` instead of
# using `DiscreteHistogramMetric` directly.
return cls("blah")
It's a bit unfortunate that we need to continue using TypeVars within the subclass instead of defining it more simply the way you did in your question -- I believe this behavior is a bug in mypy.
However, it does do the trick: doing DiscreteHistogramMetric.decode("blah")
will return a TMetricBase
as expected.
And unlike the first approach, the messiness is at least pretty well-confined to the decode
method and doesn't require you to start using generics wherever you're also using MetricBase
classes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With