This is not an easy task to solve, because in your example:
my_dict[1][2] = 3
my_dict[1] leads to a call to __getitem__ in the dictionary. At this point, there is no way to know that the task is in progress. Only the last [] in the sequence is a __setitem__ call, and it cannot be successful unless mydict[1] exists, because otherwise what object do you assign?
Therefore, do not use auto-processing. Instead, you can use setdefault() , with a regular dict .
my_dict.setdefault(1, {})[2] = 3
Now that it's not quite pretty, especially when you nest more deeply, you can write a helper method:
class MyDict(dict): def nest(self, keys, value): for key in keys[:-1]: self = self.setdefault(key, {}) self[keys[-1]] = value my_dict = MyDict() my_dict.nest((1, 2), 3) # my_dict[1][2] = 3
But itβs even better to include this in the new __setitem__ , which immediately accepts all indexes, instead of requiring intermediate __getitem__ calls that cause auto-processing. Thus, we know from the very beginning that we are doing the job and can continue without relying on auto-processing.
class MyDict(dict): def __setitem__(self, keys, value): if not isinstance(keys, tuple): return dict.__setitem__(self, keys, value) for key in keys[:-1]: self = self.setdefault(key, {}) dict.__setitem__(self, keys[-1], value) my_dict = MyDict() my_dict[1, 2] = 3
To ensure consistency, you can also provide __getitem__ , which accepts keys in a tuple as follows:
def __getitem__(self, keys): if not isinstance(keys, tuple): return dict.__getitem__(self, keys) for key in keys: self = dict.__getitem__(self, key) return self
The only drawback that I can think of is that we cannot use tuples as dictionary keys as easily: we have to write it down like, for example. my_dict[(1, 2),] .