Search code examples
pythonpython-typing

Type hinting list of child classes when the original list is of the parent's type in Python


Given a variable with the type hint list[ParentItem], how can one assign another list to it with the type hint list[ChildItem], where ChildItem is derived from ParentItem, without triggering linter type checking errors?

Consider the following contrived, minimal example in which the pyright linter throws the following argument type checking error:

Argument of type list[ChildItem] cannot be assigned to parameter items of type list[ParentItem] in function __init__
list[ChildItem] is incompatible with list[ParentItem] Type parameter _T@list is invariant, but ChildItem is not the same as ParentItem Consider switching from list to Sequence which is covariant

class ParentItem:
    def __init__(self) -> None:
        pass


class ChildItem(ParentItem):
    def __init__(self) -> None:
        super().__init__()


class ParentGroup:
    def __init__(self, items: list[ParentItem]) -> None:
        self._items = items

    def add(self, item: ParentItem) -> None:
        self._items.append(item)


class ChildGroup(ParentGroup):
    def __init__(self, child_items: list[ChildItem]) -> None:
        super().__init__(child_items)  # pyright argument type checking error here

Changing the line:

    def __init__(self, child_items: list[ChildItem]) -> None:

to

    def __init__(self, child_items: list[ParentItem]) -> None:

superficially resolves the error, but this doesn't provide the desired type hints when the ChildGroup class is used. The error reappears later anyway when one attempts to create a ChildGroup instance using a list of ChildItem instances, e.g.:

i1 = ChildItem()
i2 = ChildItem()
ilist = [i1, i2]
g_with_child_items = ChildGroup(ilist) # same error as before, but regarding assignment to "child_items" parameter instead of "items" parameter

How can the child_items variable of the ChildGroup class be correctly type annotated as list[ChildItem]?


Solution

  • The solution is to create a user-defined generic type variable (TypeVar) that is bound to the ParentItem class, and then to use this type in the signatures of the ParentGroup class' methods where applicable. This will allow ParentItem and its derived classes to be used without type hinting errors in ParentGroup and its derivatives. Note that ParentGroup has to derive from the typing.Generic base class, and then any generic types that are to be used within this class have to be specified (i.e. ParentItemType as shown below).

    from typing import Generic, TypeVar
    
    ParentItemType = TypeVar("ParentItemType", bound="ParentItem")
    
    
    class ParentItem:
        def __init__(self) -> None:
            pass
    
    
    class ChildItem(ParentItem):
        def __init__(self) -> None:
            super().__init__()
    
    
    class ParentGroup(Generic[ParentItemType]):
        def __init__(self, items: list[ParentItemType]) -> None:
            self._items = items
    
        def add(self, item: ParentItemType) -> None:
            self._items.append(item)
    
    
    class ChildGroup(ParentGroup[ChildItem]):
        def __init__(self, child_items: list[ChildItem]) -> None:
            super().__init__(child_items)
    

    The following now works as desired without linter errors:

    i1 = ChildItem()
    i2 = ChildItem()
    ilist = [i1, i2]
    g_with_child_items = ChildGroup(ilist)