Python 'get' over 'in' or 'KeyError'

created:

updated:

tags: python

I feel that I still don’t know Python that well and proper Pythonic way of doing things. One of the things I learned from a senior member of my team was to use get instead of checking KeyError by directly accessing a dictionary’s attribute. Now I’ve got more used to it and I remember to use get.

Example

fruit_basket = {
    "apple": 2,
    "banana": 5
}

When I want to find out how many apple is there, I can do the followings:

  • Accessing apple’s count by doing fruit_basket['apple']
    • In this case, if apple is not in fruit_basket, it will raise KeyError exception when we try to access the value.
    try:
        count = fruit_basket['apple']
    except KeyError:
        count = 0
    
    • Only need to access once and assign once.
  • Checking 'apple' in fruit_basket is True or False
    if 'apple' in fruit_basket:
        count = fruit_basket['apple']
    else:
        count = 0
    

This flow of fetching a key that exists or returning a default value is so common and Python’s built-in dict provides get() method (ex: fruit_basket.get('apple')) and it is often better than the above two.

Python dict’s get method

Syntax

key = 'apple'
fruit_basket.get(key, 0)

The second parameter 0 is the default value to return in case the key is not present. This also requires only one access and one assignment.

Reason

Using get is much shorter than KeyError or in examples which suffer from code duplication for assignments. For a dictionary with simple types, using get method is the shortest and clearest option.

Python dictionary with more complex type

Using in with more complex type

votes = {
    'americano': ['Paul', 'Mary'],
    'cafe latte': ['Steve', 'Casey'],
}

key = 'cappuccino'

if key in votes:
    names = votes[key]
else:
    votes[key] = names = []
  • Requires two access if the key is present.
  • Requires one access and one assignment if the key is missing.

Using KeyError exception for more complex type

try:
    names = votes[key]
except KeyError:
    votes[key] = names = []
  • Requires one access if the key is present.
  • Requires one access and one assignment if the key is missing.

Using get method for more complex type

if (names := votes.get(key)) is None:
    votes[key] = names = []
  • := is assignment expression

Using setdefault method of dict

The setdefault method tries to fetch the value of a key in the dictionary and if the key is not present, the method assigns the key to the default value provided. This can implement the same logic as in get method.

names = votes.setdefault(key, [])
  • This is shorter than using get method.

Downsides with using setdefault

  • However, the readability may not be good (‘set default’ while getting the value)
  • The default value is assigned directly into the dictionary if the key is missing instead of being copied
    data = {}
    key = 'foo'
    value = []
    data.setdefault(key, value)
    print('Before:', data)
    value.append('hello')
    print('After:', data)
    >>>
    Before: {'foo': []}
    After: {'foo': ['hello']}
    
    • This may mean that we’ll need to construct a new default value for each key, and this can be a significant performance overhead. If we re-use the object for the default value, it may lead to strange behaviour or bugs.

Use cases for setdefault

  • When the default values are cheap to construct, mutable, and there’s no potential for raising exceptions
  • Except these special cases, we may want to consider using defaultdict instead.

TLDR

  • There are four methods to detect and handle missing dictionary keys:
    • in
    • KeyError
    • get
    • setdefault
  • get method is best for dictionaries with basic types and also with dictionary values of a high cost
  • Use defaultdict instead of setdefault

References