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()
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...
Your last attempt is close. However, you weren't really using the generic to type the view in the concrete class.
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()
BaseController
template.attr
attribute).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.
A
that uses another type - B
or C
, without knowing which one in advance.BaseController
is a Generic, that takes parameter type T
and can use it across the class.T
is a TypeVar, which means it's just a type. It can be bounded to a type to match any of it's subclasses or be exactly one of listed types (see https://docs.python.org/3/library/typing.html#typing.TypeVar)ConcreteController
subclass of BaseController
, is using [ConcreteView]
to tell the ancestor which type was eventually used.ConcreteView
is checked against T
and is a legit type (in this case, T
can be any type, since nothing else is specified).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()