Reputation: 181
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?
Upvotes: 16
Views: 10390
Reputation: 177
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") }
Upvotes: 9
Reputation: 24094
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.
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])
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])
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.
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
Upvotes: 15