Jacktose
Jacktose

Reputation: 733

Type hinting Python inheritance “Base classes of [child] are mutually incompatible”

I'm trying to learn how to use base classes and inheritance. I'm getting type-checking errors, but the code is running as expected. Do I have type checker problems, type hinting problems, or meaningful code problems?

Here I try a generic class that inherits from the abstract base class Sequence (and ABC—is that necessary?). Then I make child classes where that sequence is specifically a list or a tuple. Pylance/pyright is happy with this and it runs fine:

from __future__ import annotations
from abc import ABC
from collections.abc import Iterable, Sequence

class Row[T](Sequence[T], ABC):
    def __init__(self, iterable: Iterable[T]):
        ...

class Row_Mutable[T](list[T], Row[T]):
    def __init__(self, iterable):
        super().__init__(iterable)

class Row_Immutable[T](tuple[T], Row[T]):
    def __init__(self, iterable):
        super().__init__(iterable)

row_mut_int: Row_Mutable[int] = Row_Mutable(range(10))
row_mut_str: Row_Mutable[str] = Row_Mutable('abcdefg')
row_imm_int: Row_Immutable[int] = Row_Immutable(range(10))
row_imm_str: Row_Immutable[str] = Row_Immutable('abcdefg')

Now a 2D version. This is a Sequence of Sequences, so the children are list of lists and tuple of tuples:

class Grid[T](Sequence[Sequence[T]], ABC):
    def __init__(self, iterable: Iterable[Iterable[T]]):
        ...

class Grid_Mutable[T](list[list[T]], Grid[T]):
    def __init__(self, iter_of_iter):
        super().__init__(list(row) for row in iter_of_iter)

class Grid_Immutable[T](tuple[tuple[T]], Grid[T]):
    def __init__(self, iter_of_iter):
        super().__init__(tuple(row) for row in iter_of_iter)

Now Pylance/pyright calls out the class definitions:

Base classes of Grid_Mutable are mutually incompatible
Base class "Grid[T@Grid_Mutable]" derives from "Sequence[Sequence[T@Grid_Mutable]]" which is incompatible with type "Sequence[list[T@Grid_Mutable]]"

... and the equivalent for the tuple version.

How is Sequence[Sequence[T]] incompatible with Sequence[list[T]]? How should I code and hint something like this?

Copilot suggested the T = TypeVar('T') form rather than Grid[T]; that slightly changed the errors but didn't resolve them. I'm running Python 3.13 and I'm not concerned about backwards compatibility—more about best practices and Pythonicness.

Upvotes: 0

Views: 93

Answers (3)

Daraan
Daraan

Reputation: 3947

This is not a compatible case. From a runtime perspective you (rather) want list/tuple to be your parent class, however from a typing perspective you want the Sequence methods to be your direct parents, otherwise list/tuple will be hardly-typed into your signatures and pyright picks of the incompatibilities in the signatures of Sequence (typing.pyi) and list/tuple builtins.pyi.

You can silence the incompatibilities by reversing the order


class Grid[T](Sequence[Sequence[T]], ABC):
    def __init__(self, iterable: Iterable[Iterable[T]]):
        ...

class Grid_Mutable[T](Grid[T], list[list[T]]):  # instead of list[list[T]], Grid[T]
    def __init__(self, iter_of_iter): ...

However this will make Sequence your runtime parent class.

Solutions:

  1. Use only one of them as your base class
  • subclassing builtin classes can often be avoided
  1. Separate runtime vs. type-checking by using Grid[T] if TYPE_CHECKING else list as parents, however I do not think that this is justified here
  2. Rely on duck-typing, write your Sequence/Protocol classes for static type-checking, but at runtime strictly use unmodified list/tuple objects.

Upvotes: 1

InSync
InSync

Reputation: 10673

I asked the maintainers of Pyright. Here's the reply (emphasis mine):

I think pyright is correct here. You should pick one or the other as a base class, not both.

[...]

If you swap the two base classes in your MutableGrid and ImmutableGrid examples, they becomes sound. That's because list[list[T] and tuple[tuple[T, ...]] are both subtypes of Sequence[Sequence[T]]. Ordering matters here because later base classes override earlier ones.

It is perhaps better not to inherit from list and tuple in this case, as they bring more trouble than they are worth. Instead, prefer composition:

class MutableGrid[T](Grid[T]):
    _cells: list[list[T]]

Upvotes: 1

user2357112
user2357112

Reputation: 281748

This looks like a Pylance bug. Your code is fine, and mypy accepts it without complaint.

Upvotes: 1

Related Questions