14

How can a dictionary be subclassed such that the subclass supports generic type hinting? It needs to behave like a dictionary in every way and support type hints of the keys and values. The subclass will add functions that access and manipulate the dictionary data. For example, it will have a valueat(self, idx:int) function that returns the dictionary value at a given index.

It doesn't require OrderedDict as its base class, but the dictionary does need to have a predictable order. Since OrderedDict maintains insertion order and supports type hints, it seems like a reasonable place to start. Here's what I tried:

from collections import OrderedDict

class ApplicationSpecificDict(OrderedDict[str, int]):
    ...

However, it fails with the error: TypeError: 'type' object is not subscriptable

Is this not supported in Python 3.7+, or am I missing something?

Christopher Peisert
  • 21,862
  • 3
  • 86
  • 117
Tom Jordan
  • 141
  • 1
  • 7
  • 4
    You can already type hint a regular dict with `typing.Dict[str, int]`. Were you looking for something else? – user2357112 Apr 09 '20 at 01:48
  • 2
    Not sure about the specifics of what you are trying to do, but based on what you are asking, dataclasses may be a more elegant solution to accomplish your goals. They handle the typing, default values, and have a replace() method for updating. They interoperate with dictionaries quite well too, including initializing with **dict_val and converting instance vars to a dict using its asdict() method if you really need pure dict functionality. – John S Apr 09 '20 at 02:04
  • @JohnS Thanks. I'm using `dataclass` in several modules. What is lacking in `dataclass` is the behavior of a `dict` (obviously; however one could add that by implementing `dict` methods as I've done in `TypedDict`) and also lacking support for type hinting. How would a user specify the key/value data types of the dataclass's `dict` data member? – Tom Jordan Apr 09 '20 at 12:42
  • The point of a dataclass is to not have any field but some well defined ones only. – LtWorf Apr 09 '20 at 13:07
  • @user2357112 The goal is to have class that acts like a dictionary class, but with additional methods to access and manipulate the dictionary entries consistent with the type hints of the keys and values. – Tom Jordan Apr 28 '20 at 16:20
  • It looks like this has been answered here: https://stackoverflow.com/questions/34736275/how-to-type-hint-collections-ordereddict-via-python-3-5-typing-module – ex-nerd Jul 01 '20 at 21:05
  • Does this answer your question? [how to type hint collections.OrderedDict via python 3.5 typing module](https://stackoverflow.com/questions/34736275/how-to-type-hint-collections-ordereddict-via-python-3-5-typing-module) – ex-nerd Jul 01 '20 at 21:06
  • I also vote for dataclasses (python 3.7+). https://realpython.com/python-data-classes/ – Dmitry Belaventsev Oct 12 '20 at 16:42
  • Does this answer your question? [How to add type annotations to custom dict subclass in python?](https://stackoverflow.com/questions/59427687/how-to-add-type-annotations-to-custom-dict-subclass-in-python) – Alex Oct 12 '20 at 17:17

2 Answers2

12

The typing package provides generic classes that correspond to the non-generic classes in collections.abc and collections. These generic classes may be used as base classes to create user-defined generic classes, such as a custom generic dictionary.

Examples of generic classes corresponding to types in collections.abc:

  • typing.AbstractSet(Sized, Collection[T_co])
  • typing.Container(Generic[T_co])
  • typing.Mapping(Sized, Collection[KT], Generic[VT_co])
  • typing.MutableMapping(Mapping[KT, VT])
  • typing.MutableSequence(Sequence[T])
  • typing.MutableSet(AbstractSet[T])
  • typing.Sequence(Reversible[T_co], Collection[T_co])

Examples of generic classes corresponding to types in collections:

  • typing.DefaultDict(collections.defaultdict, MutableMapping[KT, VT])
  • typing.OrderedDict(collections.OrderedDict, MutableMapping[KT, VT])
  • typing.ChainMap(collections.ChainMap, MutableMapping[KT, VT])
  • typing.Counter(collections.Counter, Dict[T, int])
  • typing.Deque(deque, MutableSequence[T])

Implementing a custom generic dictionary

There are many options for implementing a custom generic dictionary. However, it is important to note that unless the user-defined class explicitly inherits from Mapping or MutableMapping, static type checkers like mypy will not consider the class as a mapping.

Example user-defined generic dictionary

from collections import abc  # Used for isinstance check in `update()`.
from typing import Dict, Iterator, MutableMapping, TypeVar

KT = TypeVar('KT')
VT = TypeVar('VT')


class MyDict(MutableMapping[KT, VT]):

    def __init__(self, dictionary=None, /, **kwargs) -> None:
        self.data: Dict[KT, VT] = {}
        if dictionary is not None:
            self.update(dictionary)
        if kwargs:
            self.update(kwargs)
    
    def __contains__(self, key: KT) -> bool:
        return key in self.data

    def __delitem__(self, key: KT) -> None:
        del self.data[key]

    def __getitem__(self, key: KT) -> VT:
        if key in self.data:
            return self.data[key]
        raise KeyError(key)

    def __len__(self) -> int:
        return len(self.data)

    def __iter__(self) -> Iterator[KT]:
        return iter(self.data)

    def __setitem__(self, key: KT, value: VT) -> None:
        self.data[key] = value
    
    @classmethod
    def fromkeys(cls, iterable: Iterable[KT], value: VT) -> "MyDict":
        """Create a new dictionary with keys from `iterable` and values set 
        to `value`.

        Args:
            iterable: A collection of keys.
            value: The default value. All of the values refer to just a single 
                instance, so it generally does not make sense for `value` to be a 
                mutable object such as an empty list. To get distinct values, use 
                a dict comprehension instead.

        Returns:
            A new instance of MyDict.
        """
        d = cls()
        for key in iterable:
            d[key] = value
        return d

    def update(self, other=(), /, **kwds) -> None:
        """Updates the dictionary from an iterable or mapping object."""
        if isinstance(other, abc.Mapping):
            for key in other:
                self.data[key] = other[key]
        elif hasattr(other, "keys"):
            for key in other.keys():
                self.data[key] = other[key]
        else:
            for key, value in other:
                self.data[key] = value
        for key, value in kwds.items():
            self.data[key] = value

Christopher Peisert
  • 21,862
  • 3
  • 86
  • 117
  • Wow, what a nice template!!! Question: in `__getitem__()`, why the `key in self.data` test? Couldn't it simply `return self.data[key]` directly, as it is expected of `data`'s `__getitem__` to raise `KeyError` itself? – MestreLion Oct 15 '21 at 14:21
  • 3
    @MestreLion The `__getitem__()` key check is so that in the event of a `KeyError`, the error originates from `MyDict` instead of the nested `MyDict.data` (which would needlessly expose an implementation detail). – Christopher Peisert Oct 15 '21 at 17:39
8

I posted on this question which yours may be a dupe of, but I will include it here as well because I found both of these questions when I was googling how to do this.

Basically, you need to use the typing Mapping generic This is the generic annotation that dict uses so you can define other types like MyDict[str, int].

How to:

import typing
from collections import OrderedDict

# these are generic type vars to tell mutable-mapping 
# to accept any type vars when creating a sub-type of your generic dict
_KT = typing.TypeVar("_KT") #  key type
_VT = typing.TypeVar("_VT") #  value type


# `typing.MutableMapping` requires you to implement certain functions like __getitem__
# You can get around this by just subclassing OrderedDict first.
# Note: The generic you're subclassing needs to come BEFORE
# the `typing.MutableMapping` subclass or accessing indices won't work.

class ApplicationSpecificDict(
        OrderedDict, 
        typing.MutableMapping[_KT, _VT]
):
    """Your special dict"""
    ...

# Now define the key, value types for sub-types of your dict
RequestDict = ApplicationSpecificDict[str, typing.Tuple[str, str]]
ModelDict = ApplicationSpecificDict[str, typing.Any]

Now use you custom types of your sub-typed dict:

from my_project.custom_typing import ApplicationSpecificDict #  Import your custom type

def make_request() -> ApplicationSpecificDict:
    request = ApplicationSpecificDict()
    request["test"] = ("sierra", "117")
    return request

print(make_request())

Will output as { "test": ("sierra", "117") }

Alex
  • 147
  • 1
  • 9