I have the following class which maps a str to its corresponding int or None if there is no such key. I want it to be a subclass of collections.abc.MutableMapping. The real logic is a bit more complicated than just self._record.get(), but the whole thing boils down to just this:
from typing import Iterator
from collections.abc import MutableMapping
class FooMap(MutableMapping[str, int]):
_record: dict[str, int]
def __init__(self) -> None:
self._record = {}
def __contains__(self, key: str) -> bool:
return self[key] is not None
def __getitem__(self, key: str) -> int | None:
return self._record.get(key)
def __setitem__(self, key: str, value: int) -> None:
self._record[key] = value
def __delitem__(self, key: str) -> None:
raise TypeError('Keys cannot be deleted')
def __len__(self) -> int:
return len(self._record)
def __iter__(self) -> Iterator[str]:
return iter(self._record)
However, mypy complains about both the __contains__():
main.py:12: error: Argument 1 of "__contains__" is incompatible with supertype "Mapping"; supertype defines the argument type as "object" [override]
main.py:12: note: This violates the Liskov substitution principle
main.py:12: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
main.py:12: error: Argument 1 of "__contains__" is incompatible with supertype "Container"; supertype defines the argument type as "object" [override]
...and the __getitem__() method:
main.py:15: error: Return type "int | None" of "__getitem__" incompatible with return type "int" in supertype "Mapping" [override]
I get the latter: The __getitem__() method of a Mapping[str, int] is supposed to return an int, not None, but that is not my use case. Changing int to int | None doesn't help, since __setitem__()'s second argument will also need to be changed correspondingly.
The former is even more confusing: The first argument passed to __contains__() should be a str, since we are talking about a Mapping[str, int]. Yet, mypy expected something more generic than object, according to the link it gave me. I changed that to key: object, but to no avail, as __getitem__() wants a str.
I know I can just throw MutableMapping away or add a comment to explicitly tell mypy that it doesn't need to scrutinize a line, but I also don't want to do that.
How to make mypy happy while retaining my initial use case? I'm fine with using any features supported by mypy 1.4+ and Python 3.11+.
It is actually possible to
.register()a class as a "virtual subclass" of anABC:This also supports runtime typechecking:
Even though I didn't end up using this (too late to change now), it passes
mypyand fits my purposes nicely.