Reputation: 733
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
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:
Grid[T] if TYPE_CHECKING else list
as parents, however I do not think that this is justified hereSequence
/Protocol
classes for static type-checking, but at runtime strictly use unmodified list
/tuple
objects.Upvotes: 1
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 becauselist[list[T]
andtuple[tuple[T, ...]]
are both subtypes ofSequence[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
Reputation: 281748
This looks like a Pylance bug. Your code is fine, and mypy accepts it without complaint.
Upvotes: 1