# HomogeneousDict - The Python dictionary that can have homogeneous keys

I've created a subclass of Python's builtin dictionary dict. The subclass allows dictionaries to have multiple keys with the same value. Hence the name "homogeneous". Behind the scenes, I'm using a normally dictionary. If a key has more than one value, then it's values are implemented with a list. Otherwise, the key, value pair is normal.

Here are some example usages:

>>> d = HomogeneousDict([('a', 1), ('b', 2), ('c', 3), ('b', 4)])
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1})
>>> d['a']
1
>>> d['b']
[2, 4]
>>> d['a'] = 5
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5})
>>> d['a']
[1, 5]
>>> len(d)
5
>>> d['subdict'] = HomogeneousDict([('d', 6), ('e', 7)])
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5, 'subdict': HomogeneousDict({'e': 7, 'd': 6})})
>>> d['subdict'] = 8
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5, 'subdict': HomogeneousDict({'e': 7, 'd': 6}), 'subdict': 8})
>>> d['subdict']
[HomogeneousDict({'e': 7, 'd': 6}), 8]
>>> del d['subdict']
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5})
>>> d.pop('b', 1) # Pop the second value of the 'b' key
4
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'a': 1, 'a': 5})
>>> for k, v in d.items():
print('{} => {}'.format(k, v))

c => 3
b => 2
a => 1
a => 5
>>>


I haven't given much thought if there is a practically usage for this type of dictionary. I really made this as more of a hobby project, rather than out of necessity.

Here's the source code. I documented the important parts of the code, so their shouldn't be much to explain or figure out. It's actually only about 120 LOCs:

class HomogeneousDict(dict):
"""
-----

A dictionary that allows multiple keys to have
like values.

The dictionary supports all of the same methods of a normally
dictionary. The chief difference between a normally dictonary and
a HomogeneousDict, is when you get items.

If a key has be given multiple values, a list of all
of the values are returned for that key. Otherwise, the
key's value is returned normally as is.

Examaples
---------
>>> d = HomogeneousDict([('a', 1), ('b', 2), ('c', 3), ('b', 4)])
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1})
>>> d['a']
1
>>> d['b']
[2, 4]
>>> d['a'] = 5
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5})
>>> d['a']
[1, 5]
>>> len(d)
5
>>> d['subdict'] = HomogeneousDict([('d', 6), ('e', 7)])
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5, 'subdict': HomogeneousDict({'e': 7, 'd': 6})})
>>> d['subdict'] = 8
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5, 'subdict': HomogeneousDict({'e': 7, 'd': 6}), 'subdict': 8})
>>> d['subdict']
[HomogeneousDict({'e': 7, 'd': 6}), 8]
>>> del d['subdict']
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1, 'a': 5})
>>> d.pop('b', 1) # Pop the second value of the 'b' key
4
>>> d
HomogeneousDict({'c': 3, 'b': 2, 'a': 1, 'a': 5})
>>> for k, v in d.items():
print('{} => {}'.format(k, v))

c => 3
b => 2
a => 1
a => 5
>>>

"""
def __init__(self, values):
"""
Parameters
----------
values : list
List of tuples that represent key, value pair
"""
for key, value in values:
self.__setitem__(key, value)

def __setitem__(self, key, value):
if key in self:
if isinstance(self[key], list):
self[key].append(value)
else:
super().__setitem__(key, [self.pop(key), value])
else:
super().__setitem__(key, value)

def __getitem__(self, key):
return super().__getitem__(key)

def __len__(self):
count = 0
for key, value in super().items():
if isinstance(value, list):
count += len(value)
else:
count += 1
return count

def __repr__(self):
key_value_reprs = []
for key, value in super().items():
if isinstance(value, list):
for element in value:
repr_ = '{}: {}'.format(key.__repr__(), element.__repr__())
key_value_reprs.append(repr_)
else:
repr_ = '{}: {}'.format(key.__repr__(), value.__repr__())
key_value_reprs.append(repr_)
return 'HomogeneousDict({%s})' % ', '.join(key_value_reprs)

def pop(self, key, index=0):
if isinstance(self[key], list):
value = self[key].pop(index)
if len(self[key]) == 1:
super().__setitem__(key, self[key].pop())
return value
else:
return super().pop(key)

def items(self):
for key, value in super().items():
if isinstance(value, list):
for element in value:
yield (key, element)
else:
yield key, value


The biggest question I have is: How well did I emulate a Python container type. Is there anything unintuitive or non-normally for a dictionary-like container type? Also, is there a better name for a dictionary that behaves like this?

1. __repr__ should:

look like a valid Python expression that could be used to recreate an object with the same value (given an appropriate environment).

Yours cannot possibly do that, as creating the vanilla dictionary loses all but one of the values for each key. Also note you can write:

repr_ = '{!r}: {!r}'.format(key, element)

2. In terms of emulating container types, you can look at the ABCs to see what needs to be implemented. I would probably have approached this with composition rather than inheritance, leveraging the ABCs for automatic implementation of some methods:

class HomogeneousDict(MutableMapping):


This helps avoid missing important methods; for example, you don't provide a matching implementation of .values.

3. In terms of naming, this is generally called a MultiDict.

4. Given the examples, I'd add the following to the end of the file:

if __name__ == '__main__':
import doctest
doctest.testmod()


This won't impact anything importing the class, but means that the behaviour is validated against your expectations if you run the file directly, using doctest. This may mean tweaking things slightly to avoid issues with order, etc.; for example:

File "so.py", line 20, in __main__.HomogeneousDict
Failed example:
d
Expected:
HomogeneousDict({'c': 3, 'b': 2, 'b': 4, 'a': 1})
Got:
HomogeneousDict({'a': 1, 'c': 3, 'b': 2, 'b': 4})


You will also need to update multi-line examples with a leading ellipsis:

>>> for k, v in d.items():
...     print('{} => {}'.format(k, v))
...
c => 3

• Thanks for the answer. Believe it or not, "Multidict" was one of the names I considered. But I thought it sounded generic and didn't really convey the dictionary's behavior. But I figured someone had made something like this before, so I guess "Multidict" would work. Commented Jul 14, 2017 at 12:13
1. The docstring says:

A dictionary that allows multiple keys to have
like values.


but this isn't right. It's a dictionary that allows each key to have multiple values.

2. Normally we don't need a specialized data structure for this use case because collections.defaultdict is good enough:

>>> from collections import defaultdict
>>> d = defaultdict(list) # mapping from key to list of values
>>> d[1].append(2)
>>> d[1].append(3)
>>> d[1]
[2, 3]
>>> d[2].append(4)
>>> d
defaultdict(<class 'list'>, {1: [2, 3], 2: [4]})
>>> d[1].pop()
3
>>> d[1].pop()
2
>>> d[1].pop()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: pop from empty list


Note that if we want different semantics for the collections of values, we can easily switch to defaultdict(set) or defaultdict(deque) or whatever.