Search code examples
pythonpython-typingmypy

Typing error with inherited classes having overloaded constructor with different parameters


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:

  • It involves defining a type alias (TConcreteChild) which is otherwise useless
  • The definition of this alias must be updated if more child classes are defined
  • It involves defining types before definitions of the classes, so it requires forward references using strings (and therefore it can't use the new ChildA | ChildB syntax).

How can I do this more cleanly, while still having strict type checking?


Solution

  • 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:
        ...