Execute Program

Python for Programmers: Missing Dictionary Keys

Welcome to the Missing Dictionary Keys lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • When working with dictionaries, we often have to deal with missing keys. As usual, Python is strict compared to many other dynamic languages. Accessing a key that doesn't exist raises a KeyError.

  • >
    phone_numbers = {
    "Amir": "555-1234",
    "Cindy": "601-5362"
    }
    phone_numbers["Betty"]
    Result:
    KeyError: 'Betty'Pass Icon
  • There are many ways to handle this situation, and different languages have different idiomatic solutions. In this lesson, we'll explore four different ways to manage missing dictionary keys, going from least to most idiomatic in Python.

  • Solution 1: when a user exists but doesn't have a phone number, we put them in the dictionary with a None value.

  • >
    phone_numbers = {
    "Amir": "555-1234",
    "Cindy": "601-5362",
    "Betty": None
    }
    phone_numbers["Betty"]
    Result:
    NonePass Icon
  • Depending on the circumstances, this may make sense, but in general this isn't idiomatic in Python.

  • The problem is that it complicates our mental model of the dictionary. Previously, phone_numbers[name] always gave us a string. Now, phone_numbers[name] gives us a string or a None, so we always have to consider both of those cases.

  • For example, we might do len(phone_numbers[name]). If phone numbers can be None, then that becomes len(None), which is a TypeError.

  • >
    phone_numbers = {
    "Amir": "555-1234",
    "Cindy": "601-5362",
    "Betty": None
    }

    def phone_number_length(name):
    return len(phone_numbers[name])

    phone_number_length("Betty")
    Result:
    TypeError: object of type 'NoneType' has no len()Pass Icon
  • This becomes an even bigger problem when the phone_numbers[name] and len(...) calls occur in different source files. When that happens, it can be difficult to understand where the None originally came from.

  • Solution 2: we can use in to decide whether the name is in the dictionary.

  • >
    phone_numbers = {
    "Amir": "555-1234",
    "Cindy": "601-5362",
    }

    def phone_number_length(name):
    if name in phone_numbers:
    return len(phone_numbers[name])
    else:
    return 0

    phone_number_length("Betty")
    Result:
    0Pass Icon
  • This is an improvement. Now that all the values are strings, we can safely do len(phone_numbers[name]). Or, if we need a list of the phone numbers, we can do list(phone_numbers.values()) without worrying about whether there are any Nones in that list.

  • We'll still get an exception when we do phone_numbers[name] for a user who doesn't exist in the dictionary. But at least that happens immediately, and the exception traceback will point to the line where we tried to access the key.

  • Solution 3 is similar to what we just did, but instead of the if, we try to access the key in a try/except. We catch the KeyError if one happens, then handle the missing key inside of the except: block.

  • >
    phone_numbers = {
    "Amir": "555-1234",
    "Cindy": "601-5362",
    }

    def phone_number_length(name):
    # We could check for the key with `in`. But in Python, it's idiomatic to
    # simply access the key, then catch the `KeyError` if it happens.
    try:
    return len(phone_numbers[name])
    except KeyError:
    return 0

    phone_number_length("Betty")
    Result:
    0Pass Icon
  • Using exceptions for control flow like this is considered poor style in many languages. In those languages, the thinking is that exceptions should be used for genuine errors, not for handling normal situations like this. However, "exceptions should be used for genuine errors" is only a convention, and not all languages follow that convention.

  • Using exceptions for control flow is idiomatic in the Python community, especially when indexing into dicts and lists! Python even has an acronym for this: EAFP ("it's Easier to Ask for Forgiveness than Permission"). In programming terms, it's easier to index by the dict key and handle the exception when it happens, rather than checking for the key up front.

  • Solution 4: Use the dictionary .get method. some_dict.get(key) returns the value for that key if it exists, or None if doesn't.

  • >
    empty_dict = {}
    empty_dict.get("Betty")
    Result:
    NonePass Icon
  • At first this approach seems to have the same issue as Solution 1: we have to account for both string and None values. Fortunately, .get takes a default value as its optional second argument. If the key doesn't exist, .get returns that default instead.

  • >
    empty_dict = {}
    empty_dict.get("Betty", "")
    Result:
    ''Pass Icon
  • We can use that to write phone_number_length in one line, without an if or a try.

  • >
    phone_numbers = {
    "Amir": "555-1234",
    "Cindy": "601-5362"
    }

    def phone_number_length(name):
    return len(phone_numbers.get(name, ""))

    phone_number_length("Betty")
    Result:
    0Pass Icon
  • This is our preferred solution. It gracefully handles missing keys, while being shorter than the other solutions. Some Python programmers might prefer solution 3, since it's more explicit that the key might be missing. Either approach is fine.

  • There are many ways to work with missing keys. When possible, we recommend using the .get method with a default argument. But don't be afraid to simply access a key and catch the KeyError when it doesn't exist; that's also fine in Python!