In line with fail-fast philosophy, dict access with d[k] raises an error if k is not an existing key. A common solution is d.get(k, default) which return a default rather handling KeyError if k is not present.

This post introduces three alternatives - setdefault, defaultdict and __mising for this situation.

setdefault

word = 'helloapplepineworld'
index = {}
for i, ch in enumerate(word):
    index.setdefault(ch, []).append(i)

# {'h': [0], 'e': [1, 9, 13], 'l': [2, 3, 8, 17], 'o': [4, 15], 'a': [5], 'p': [6, 7, 10], 'i': [11], 'n': [12], 'w': [14], 'r':[16], 'd': [18]}
print(index)

index.setdefault(ch, []).append(i) is the same as running

if ch not in index:
    index[ch] = []
index[ch].append(i)

except the later code performs at least two searches for ch - three if not found, which setdefault does it all with a single lookup.

defaultdict

Sometimes it is convenient to have mappings that return some made-up value when a missing key is searched. defaultdict is created for doing this.

When instantiating a defaultdict you provide a callable used to produce a default value whenever __getitem__ is passed a non-existent key argument.

For example, given an empty defaultdict created as d = defaultdict(list), if k is not in d then the expression d[k] does the following steps:

  1. calls list() to create a new list
  2. inserts the list into d using k as key
  3. returns a reference to that list
from collections import defaultdict
word = 'helloapplepineworld'
index = defaultdict(list)
for i, ch in enumerate(word):
    index[ch].append[i]

Implementing __missing__

__missing__ method is not defined in the base dict class, but if you subclass dict and provide a __missing__ method, the standard dict.__getitem__ will call it whenever a key is not found instead of rasing a KeyError.

class StrKeyDict(dict):
    def __missing__(self, key):
        if isinstance(key, str):
            raise KeyError(key)

        return self[str(key)]

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default

    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()

d = StrKeyDict([('2','two'),('4','four')])
print(d['2'], d[4]) # two four

Reference

Fluent Python