Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typing hint for abstract class method that returns class instance

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")
like image 478
IceCube Avatar asked Jun 16 '19 16:06

IceCube


People also ask

Can abstract classes have instance methods?

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 .

How do you type hints in Python?

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.

What is typing in Python?

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.

What are type hints?

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.

Can we create an instance of an abstract class?

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.

How to use type hints when renaming a class?

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.

Which abstract class declares one abstract method find?

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.

What is the use of none as a type hint?

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:


1 Answers

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:

  1. Give up and not use TypeVars
  2. Make MetricBase a generic class and have your subclasses inherit a parameterized version of MetricBase.
  3. Use 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.

like image 174
Michael0x2a Avatar answered Sep 27 '22 23:09

Michael0x2a