Search code examples
pythonabstract-classpython-typing

Correct attribute types of abstract class implementation


Sorry for the confusing title, but I'm still finding the good way to describe my case. Basically, I want to inherit from an abstract class that takes another abstract class as argument, but how can I make the attributes of its implementation recognized correctly its argument types?

Is it confusing? Here is what I mean:

from abc import ABC
from abc import abstractmethod
from dataclasses import dataclass


# Define abstract classes
class Params(ABC):
    """An abstract class for storing parameters."""

    pass


class Solver(ABC):
    """An abstract class to solve a problem with a specific parameter set."""

    def __init__(self, params: Params) -> None:
        """Initialize with a Params object."""
        self.params = params

    @abstractmethod
    def solve(self) -> None:
        """An abstract method that must be implemented in the subclasses."""
        pass


# Implementation of abstract classes
@dataclass
class ParamsX(Params):
    """A subclass of Params which has attributes first_param and second_param."""

    first_param: int
    second_param: list[float]


class SolverX(Solver):
    """A subclass of Solver which solve the problem X."""

    def __init__(self, params: ParamsX) -> None:
        """Initialize with the ParamsX object."""
        super().__init__(params)

    def solve(self) -> None:
        """Solve the problem X."""
        print(self.params)  # The params is recognized as Params
        print(self.params.first_param)  # The first_param is recognized as Any
        print(self.params.second_param)  # The second_param is recognized as Any


# There're also ParamsY and SolverY, ParamsZ and SolverZ, etc

My question: How can I make the first_param and second_param show respectively as the int and list[float] types? I haven't check with mypy but the Visual Studio Code indicates that.

An attempt:

If I do with the following code, the types are correct but I don't think it's a pythonic way, it will be like the abstract class Params doesn't have any purpose at all.

class SolverX(Solver):
    """A subclass of Solver which takes a ParamsX object as argument."""

    def __init__(self, params: ParamsX) -> None:
        super().__init__(params)
        self.params: ParamsX  # Forced annotation

Solution

  • A big thanks to TeamSpen210 who gave me this answer:

    What you need is a Generic, not necessarily an ABC.

    Change Solver to be like this:

    ParamT = TypeVar('ParamT', bound=Params)
    
    class Solver(ABC, Generic[ParamT]):
        def __init__(self, param: ParamT) -> None:
            self.params = param
    
    class SolverX(Solver[ParamsX]):
        ....
    

    TypeVar defines a type variable, which you later substitute with a specific type, bound restricts it to only permit subclasses of the specified type. Then having the class "subclass" Generic means that the variable is scoped to the whole class. So then when you use the subscripted version, it's as if the type variable was replaced by the real class.

    Note the other way you can use it is specific to a function - you put it in the signature, then it takes on whatever type you use to call the function.

    The MyPy docs have a lot more info about how things work, as well as say PEP484: https://mypy.readthedocs.io/en/stable/generics.html

    There's also typeshed, the type definitions for the standard library: https://github.com/python/typeshed/tree/master/stdlib