I want to define a typing protocol for a custom mapping class. This class needs to be very similar to a MutableMapping
, except that it has a couple of additional methods beyond those those that collections.abc.MutableMapping
defines as abstract methods (specifically .copy()), and I also want to specify in advance which types any implementations of the custom mapping class must use as keys and values.
To define this protocol I have to do this:
from collections.abc import KeysView, ValuesView, ItemsView, Mapping
from typing import Protocol, TypeVar, Optional, Tuple, runtime_checkable, Iterator, Any, overload, Iterable, Dict, Hashable
TCopyableMutableMapping = TypeVar("TCopyableMutableMapping", bound="CopyableMutableMapping")
K = TypeVar("K") # keys type
V = TypeVar("V") # values type
@runtime_checkable
class CopyableMutableMapping(Protocol[K, V]):
"""
Protocol for the type behaviour of a class which acts essentially like a mutable mapping plus a copy method.
(It would be nice to inherit from MutableMapping[K, V] so that we didn't have to write out all these
abstract typed methods, but protocols can only inherit from protocols, and MutableMapping is not a Protocol.)
"""
def __len__(self) -> int:
...
def __iter__(self) -> Iterator[K]:
...
def __contains__(self, key: K) -> bool:
...
def __getitem__(self, key: K) -> V:
...
def get(self, key: K, default: Optional[V]):
...
def keys(self) -> KeysView[K]:
...
def items(self) -> ItemsView[K, V]:
...
def values(self) -> ValuesView[V]:
...
def __eq__(self, other: Any) -> bool:
...
def __setitem__(self, key: K, value: V):
...
def __delitem__(self, key: K):
...
@overload
def pop(self, key: K) -> V:
...
@overload
def pop(self, key: K, default: V = ...) -> V:
...
def pop(self, key, default=None):
...
def popitem(self) -> Tuple[K, V]:
...
@overload
def update(self, other: Mapping[K, V], **kwargs: V) -> None:
...
@overload
def update(self, other: Iterable[Tuple[K, V]], **kwargs: V) -> None:
...
@overload
def update(self, **kwargs: V) -> None:
...
def update(self, other, **kwargs):
...
def copy(self: TCopyableMutableMapping) -> TCopyableMutableMapping:
...
Notice I have deliberately overloaded some of the methods so that they match what I think is the type behaviour of the builtin dict
type.
The point of this CopyableMutableMapping
is that I should be able to use it anywhere that dict
is the expected type, because structurally I am defining my type to have the same type behaviour as dict
, and Protocol
defines my type through structural typing.
def updates_dict_like(d: CopyableMutableMapping[Hashable, int], **kwargs: int):
"""Function which I want to accept both Dict or CopyableMutableMapping types."""
d.update(**kwargs)
return d
d: Dict[Hashable, int] = {}
updates_dict_like(d)
Unfortunately this doesn't seem to work - mypy blames the overloaded methods, but I can't see how they differ from dict. (I also haven't been able to find the exact way that the typing stubs for dict
to compare to, only for Dict
.)
copyablemutablemapping.py:100: error: Argument 1 to "updates_dict_like" has incompatible type "Dict[Hashable, int]"; expected "CopyableMutableMapping[Hashable, int]"
copyablemutablemapping.py:100: note: Following member(s) of "Dict[Hashable, int]" have conflicts:
copyablemutablemapping.py:100: note: Expected:
copyablemutablemapping.py:100: note: @overload
copyablemutablemapping.py:100: note: def update(self, other: Mapping[Hashable, int], **kwargs: int) -> None
copyablemutablemapping.py:100: note: @overload
copyablemutablemapping.py:100: note: def update(self, other: Iterable[Tuple[Hashable, int]], **kwargs: int) -> None
copyablemutablemapping.py:100: note: <1 more overload not shown>
copyablemutablemapping.py:100: note: Got:
copyablemutablemapping.py:100: note: @overload
copyablemutablemapping.py:100: note: def update(self, Mapping[Hashable, int], **kwargs: int) -> None
copyablemutablemapping.py:100: note: @overload
copyablemutablemapping.py:100: note: def update(self, Iterable[Tuple[Hashable, int]], **kwargs: int) -> None
Found 1 error in 1 file (checked 1 source file)
What do I need to change to make my custom class be accepted anywhere that expects a dict?