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'
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
Nonevalue.>
phone_numbers = {"Amir": "555-1234","Cindy": "601-5362","Betty": None}phone_numbers["Betty"]Result:
None
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 aNone, so we always have to consider both of those cases.For example, we might do
len(phone_numbers[name]). If phone numbers can beNone, then that becomeslen(None), which is aTypeError.>
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()
This becomes an even bigger problem when the
phone_numbers[name]andlen(...)calls occur in different source files. When that happens, it can be difficult to understand where theNoneoriginally came from.Solution 2: we can use
into 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 0phone_number_length("Betty")Result:
0
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 dolist(phone_numbers.values())without worrying about whether there are anyNones 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 atry/except. We catch theKeyErrorif one happens, then handle the missing key inside of theexcept: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 0phone_number_length("Betty")Result:
0
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
.getmethod.some_dict.get(key)returns the value for that key if it exists, orNoneif doesn't.>
empty_dict = {}empty_dict.get("Betty")Result:
None
At first this approach seems to have the same issue as Solution 1: we have to account for both string and
Nonevalues. Fortunately,.gettakes a default value as its optional second argument. If the key doesn't exist,.getreturns that default instead.>
empty_dict = {}empty_dict.get("Betty", "")Result:
''
We can use that to write
phone_number_lengthin one line, without anifor atry.>
phone_numbers = {"Amir": "555-1234","Cindy": "601-5362"}def phone_number_length(name):return len(phone_numbers.get(name, ""))phone_number_length("Betty")Result:
0
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
.getmethod with a default argument. But don't be afraid to simply access a key and catch theKeyErrorwhen it doesn't exist; that's also fine in Python!