With the code below:
import abc
class ABCParent(metaclass=abc.ABCMeta):
def __init__(self, a: str, sibling_type: type[ABCParent]) -> None:
self.a = a
self._sibling_type = sibling_type
def new_sibling(self, a: str) -> ABCParent:
return self._sibling_type(a)
class ChildA(ABCParent):
def __init__(self, a: str) -> None:
super().__init__(a, ChildB)
class ChildB(ABCParent):
def __init__(self, a: str) -> None:
super().__init__(a, ChildA)
I get the following typing error, using mypy in strict mode.
src/demo_problem.py: note: In member "new_sibling" of class "ABCParent":
src/demo_problem.py:10:16:10:36: error: Missing positional argument "sibling_type" in call to "ABCParent" [call-arg]
return self._sibling_type(a)
This is perfectly logical, since ABCParent
requests the sibling_type
argument. But in my case, the self._sibling_type
will be one of the child classes, which override the constructor such that sibling_type
is not needed.
I was able to work around the problem by using a typing.Union
of the child classes:
import abc
import typing
TConcreteChild: typing.TypeAlias = typing.Union["ChildA", "ChildB"]
class ABCParent(metaclass=abc.ABCMeta):
def __init__(self, a: str, sibling_type: type[TConcreteChild]) -> None:
...
But I don't like this because:
TConcreteChild
) which is otherwise uselessChildA | ChildB
syntax).How can I do this more cleanly, while still having strict type checking?
So, I think one approach that could work depending on the specifics of your actual use-case is making ._sibling_type
a generic callable and making the classes generic as well, parametrized with the same type variable.
import abc
import typing
P = typing.TypeVar("P", bound="ABCParent")
class ABCParent(typing.Generic[P], metaclass=abc.ABCMeta):
def __init__(self, a: str, sibling_type: typing.Callable[[str], P]) -> None:
self.a = a
self._sibling_type = sibling_type
def new_sibling(self, a: str) -> P:
return self._sibling_type(a)
class ChildA(ABCParent["ChildB"]):
def __init__(self, a: str) -> None:
super().__init__(a, ChildB)
class ChildB(ABCParent["ChildA"]):
def __init__(self, a: str) -> None:
super().__init__(a, ChildA)
Types are callables. And in my experience with mypy
, treating them like callables when you can is the easier approach.
As an aside, you can still use the pipe operator syntax with string-based forward references, e.g.:
def foo(x: "ChildA | ChildB") -> None:
...