pythonpycharmpython-typing

type hinting super().__init__ call with concrete types


Below are two class trees. Each with a base class eating the base class of its neighbor class.

Once I derivate a concrete class, I also use the neighbor type as a dependency. The code works as expected, however, the typechecker complains. (I'm using pycharm)

I think the downcast from concrete to base type is okay, as the base class will always only work with the downcasted type and is not relying on the concrecte aspects of the concrete type.

from typing import reveal_type


class BaseView:
    ...


class BaseController:
    def __init__(self, view: BaseView):
        self.view = view


class ConcreteView(BaseView):
    attr = "42"


class ConcreteController(BaseController):
    def __init__(self, view: ConcreteView):
        super().__init__(view)

    def meth(self):
        # gives an
        # Unresolved attribute reference 'attr' for class 'BaseView'
        # warning
        print(self.view.attr)
        # okay
        print(reveal_type(self.view))


if __name__ == '__main__':
    concrete_view = ConcreteView()
    controller = ConcreteController(concrete_view)
    controller.meth()

A half solution is to use factories that convince the typchecker However, the linter still is not able to resolve. Additional the factory pattern is not what I like and it makes things complicated just to satisfy the typchecker

from typing import reveal_type, Callable


class BaseView:
    ...


class BaseController:
    def __init__(self, view_factory: Callable):
        self.view = view_factory()


class ConcreteView(BaseView):
    attr = "42"

    def __init__(self):
        self.b = 10


class ConcreteController(BaseController):
    def __init__(self, view_factory: Callable[[], ConcreteView]):
        super().__init__(view_factory)

    def meth(self):
        # warning gone but lint still not working
        print(self.view.attr)

        print(self.view.b)
        # okay
        print(reveal_type(self.view))


if __name__ == '__main__':
    controller = ConcreteController(lambda: ConcreteView())
    controller.meth()

Additionally, I already played around with generic.

from typing import reveal_type, Generic, TypeVar

T = TypeVar("")


class BaseView(Generic[T]):
    ...


class BaseController(Generic[T]):
    def __init__(self, view: T):
        self.view = view


class ConcreteView(BaseView):
    attr = "42"


class ConcreteController(BaseController):
    def __init__(self, view: BaseView[ConcreteView]):
        super().__init__(view)

    def meth(self):
        # no warning typchecker
        print(self.view.attr)
        # but no lint
        self.view
        # okay
        print(reveal_type(self.view))


if __name__ == '__main__':
    concrete_view = ConcreteView()
    controller = ConcreteController(concrete_view)
    controller.meth()

Further inheritance

Based on the answer of mr_mo To keep further inheritance on sub concrete class generic, I need to add a generic to the controller

from typing import Generic, TypeVar

T = TypeVar("T")


class BaseView:
    ...


class BaseController(Generic[T]):
    def __init__(self, view: T) -> None:
        self.view = view


class ConcreteView(BaseView):
    attr = "Concrete View"


class MoreConcreteView(ConcreteView):
    attr = "More concrete view"
    foo = "another attribute of more concrete view"


class ConcreteController(Generic[T], BaseController[T | ConcreteView]):
    def __init__(self, view: ConcreteView) -> None:
        super().__init__(view)

    def meth(self) -> None:
        print(self.view.attr)
        print(type(self.view))


class MoreConcreteController(Generic[T], ConcreteController[T | MoreConcreteView]):
    def __init__(self, view):
        super().__init__(view)

    def meth(self):
        print(self.view.foo)
        print(self.view.attr)
        print(type(self.view))


if __name__ == "__main__":
    concrete_view = ConcreteView()
    controller = ConcreteController(concrete_view)
    controller.meth()
    mcv = MoreConcreteView()
    mcc = MoreConcreteController(mcv)
    mcc.meth()

looks like I still have not understood the conecpt of generics in python...


Solution

  • Your last attempt is close. However, you weren't really using the generic to type the view in the concrete class.

    Solution

    from typing import Generic, TypeVar
    
    T = TypeVar("T")
    
    
    class BaseView:
        ...
    
    
    class BaseController(Generic[T]):
        def __init__(self, view: T) -> None:
            self.view = view
    
    
    class ConcreteView(BaseView):
        attr = "42"
    
    
    class ConcreteController(BaseController[ConcreteView]):
        def __init__(self, view: ConcreteView) -> None:
            super().__init__(view)
    
        def meth(self) -> None:
            # no warning typchecker
            # now lint is working!
            print(self.view.attr)
            self.view
            # okay
            print(type(self.view))
    
    
    if __name__ == "__main__":
        concrete_view = ConcreteView()
        controller = ConcreteController(concrete_view)
        controller.meth()
    
    

    Notes

    Edit: generic subclass of a generic class

    This is added due to the addition to the original question - how do I keep advanced implementations of generic classes, generic themselves. More precisely, I'll try to TL;DR how generics are expected to be used according to official documentations in common linters (e.g. MyPy).

    Generic types have one or more type parameters, which can be arbitrary types. For example, dict[int, str] has the type parameters int and str, and list[int] has a type parameter int.

    Additionally to the typing logic, we should consider how subclasses are being used. Since python basically allows you to do almost anything, there is not obligation of typing correctly. Running strict type checkers e.g. MyPy or Pyre can help with that.

    Consider the following statements, assuming these are our rules of how things should be

    Going with that spirit, one needs to decide if the base class can be a generic class, that can handle objects of type BaseView and all of it's childs. That is BaseController.

    Then, ConcreteController can be an implementation that can handle ConcreteView and all of his childs, or even the parent. That really depends on what you are trying to achieve. For that to be generic, you need to make the ConcreteController generic again.

    This is implemented in the following example:

    from typing import Generic, TypeVar
    
    
    class BaseView:
        ...
    
    
    # We want to create a class typed with any subclass of BaseView
    T = TypeVar("T", bound=BaseView)
    
    
    class BaseController(Generic[T]):  # this class is generic
        def __init__(self, view: T) -> None:  # on init, gets the type that was used by the subclass
            self.view = view
    
    
    class ConcreteView(BaseView):  # here we want a new class (and type) - a concrete view, with attr
        attr = "42"
    
    
    # again, we want a concrete implementation, that is generic to any subtype of this class
    TC = TypeVar("TC", bound=ConcreteView)
    
    
    # so we implement based controller, with a type of and subclass of concrete view
    # but this class is also generic
    class ConcreteController(Generic[TC], BaseController[TC]):
        def __init__(self, view: TC) -> None:
            super().__init__(view)
    
        def meth(self) -> None:
            # no warning typchecker
            # lint is working now!
            print(self.view.attr)
            print(self.view)
            # okay
            print(type(self.view))
    
    
    class MoreConcreteView(ConcreteView):
        attr = "More concrete view"
        foo = "another attribute of more concrete view"
    
    
    # finally, this is the most concrete implementation.
    class MoreConcreteController(ConcreteController[MoreConcreteView]):
        def __init__(self, view: MoreConcreteView) -> None:
            super().__init__(view)
    
        def meth(self) -> None:
            # no warning typchecker
            # lint is working now!
            print(self.view.attr)  # linter finds attr
            print(self.view.foo)  # linter finds foo
            print(type(self.view))
    
    
    if __name__ == "__main__":
        concrete_view = ConcreteView()
        controller = ConcreteController(concrete_view)
        controller.meth()