0

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?

ThomasNicholas
  • 1,273
  • 11
  • 21

1 Answers1

0

I don't know why, but this seems to work:

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


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] = {1:2}

updates_dict_like(d)
hussic
  • 1,816
  • 9
  • 10