Ways to write a dictionary/lookup in Python

Here are some ways you can write a lookup in Python. Not all of these patterns are obviously a lookup, but all of them can be translated to one another, so it's worth to know these patterns. That way you can make a conscious decision to choose a specific form. Thanks to Ori for inspiring this post. Our talk about state machines was enlightening!

Different ways to look up

Using a dictionary

Classic.

    # main.py
    name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
    print(name_to_age['Alice'])  # Raise KeyError if Alice not found
    print(name_to_age.get('Alice', 0))  # Print 0 if Alice not found

As function/if-else statement

Every dictionary corresponds to an if-else statement and vice-versa. Rewriting between the two can sometimes come in handy. The if-else is a lot more explicit, which makes it easier to debug, but it's also a lot more verbose. Additionally, a dictionary lookup cannot be anything but a lookup, whereas the if-else is more general and can do pretty much anything it wants, including deleting your system32 or /bin/.

    # main.py
    def name_to_age(name: str) -> int:
        if name == 'Alice':
            return 37
        elif name == 'Bob':
            return 21
        elif name == 'Charlie':
            return 59
        else:
            # return 0 if you want to reproduce name_to_age.get behaviour
            raise NameError(f'{name=} not found!')

    print(name_to_age('Alice'))

Translation between lists

Sometimes data is in separate lists (or series, or whatever). This is a simple lookup pattern, just make sure you give the index a good name.

    # main.py
    names = ['Alice', 'Bob', 'Charlie']
    ages = [37, 21, 59]
    i_name = names.index('Alice')
    print(ages[i_name])

If you find yourself doing this a lot with the same data, consider making a helper so you don't need to deal with indexes.

    name_to_age = dict(zip(names, ages))
    print(name_to_age('Alice'))

Zipped list

This scenario is also relevant when you have a list of objects. Like Person with attributes name and age.

    # main.py
    names_and_ages = [('Alice', 37), ('Bob', 21), ('Charlie', 59)]
    alice_matches = [age for name, age in names_and_ages if name == 'Alice']
    if len(alice_matches) != 1:
        raise ValueError(f'Unexpected number of matches for Alice, got {alice_matches}')
    print(alice_matches[0])

Here you can also quickly make a helper.

    name_to_age = {name: age for name, age in names_and_ages}  # verbose but explicit
    # name_to_age = dict(names_and_ages)  # short
    print(name_to_age['Alice'])

Data hiding in index

Here, the age is encoded by the index of the name in the list. It's a little weird, but this is how you might encode a game of life grid, for instance.

    # main.py
    name_by_age = [None for _ in range(21)] + ['Bob'] + [None for _ in range(37-21-1)] + ['Alice'] + [None for _ in range(59-21-2)] + ['Charlie']
    print(name_by_age.index('Alice'))

Flipping data will also work here. Maybe filter out the Nones.

    name_to_age = {name: i for i, name in enumerate(name_by_age) if name is not None}
    print(name_to_age['Alice'])

Data hiding in names

Imports always do a lookup from name to code. Not seen often, but not too far out there in my opinion.

    # alice.py
    age = 37
    
    # bob.py
    age = 21

    # charlie.py
    age = 59

    # main.py
    from alice import age
    print(age)

Alternatively you might have something like this.

    # ages.py
    alice_age = 37
    bob_age = 21
    charlie_age = 59
    
    # main.py
    from ages import alice_age
    print(alice_age)

Array could be a dictionary

This is often useful in Advent of Code if you have some kind of 2d grid. The most intuitive data structure is a list of lists or a Numpy array.

    grid = [[1 if x % 2 == 0 and y % 7 == 0 else 0 for x in range(20)] for y in range(20)]
    print(grid[7][8])

But this might not be the best solution. Doing things in this array means looking up a coordinate pair, so we can also use a dictionary.

    grid = {(x, y): 1 if x % 2 == 0 and y % 7 == 0 else 0 for x in range(20) for y in range(20)}
    print(grid[7, 8])

Other dictionary patterns

Default value if key not found

Sometimes you may write something like this.

    name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
    if 'Dilbert' in name_to_age:
        dilbert_age = name_to_age['Dilbert']
    else:
        dilbert_age = 88

Already mentioned up above, you can use get to provide a default argument instead.

    name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
    print(name_to_age.get('Dilbert', 88))

defaultdict and inverting

Say you need to know who is 37.

    name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59}
    age_to_name = {v: k for k, v in in name_to_age.items()}
    print(age_to_name[37])

This will only work if your values are unique. If Dilbert is also 37, you are not going to find Alice in age_to_name. The solution is to keep a list of keys that map to this value, like so.

    name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59, 'Dilbert': 37}
    age_to_name = {v: [] for v in name_to_age.values()}
    for name, age in name_to_age.items():
        age_to_name[age].append(name)

    print(age_to_name[37])

Use defaultdict for this, so you don't need to loop over your collection twice. A defaultdict puts a value provided by the function you give it into the dictionary if it hasn't seen the key before.

    from collections import defaultdict

    name_to_age = {'Alice': 37, 'Bob': 21, 'Charlie': 59, 'Dilbert': 37}
    age_to_name = defaultdict(list)
    for name, age in name_to_age.items():
        age_to_name[age].append(name)

    print(age_to_name[37])
home