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+.

1

There are 1 best solutions below

0
InSync On BEST ANSWER

It is actually possible to .register() a class as a "virtual subclass" of an ABC:

from collections.abc import MutableMapping

class FooMap:
  ...

MutableMapping.register(FooMap)

This also supports runtime typechecking:

print(isinstance(FooMap(), MutableMapping))  # True

Even though I didn't end up using this (too late to change now), it passes mypy and fits my purposes nicely.