Checking Python Dict Keys Is Simple. Right?

This program doesn't work:

  1. favorites = {
  2. "appetizer": "calamari",
  3. "vegetable": "broccoli",
  4. "beverage": "coffee",
  5. }
  6. def describe(category):
  7. print("My favorite {} is {}.".format(
  8. category, favorites[category]))
  9. categories = ['dessert', 'beverage', 'vegetable', 'soup']
  10. for category in categories:
  11. describe(category)

Run, and it blows up on you:

Traceback (most recent call last):
  File "", line 13, in 
  File "", line 9, in describe
    category, favorites[category]))
KeyError: 'dessert'

Broadly, you have three ways to fix this problem. First is the "key in dict" idiom:

  1. def describe(category):
  2. if category in favorites:
  3. message = "My favorite {} is {}.".format(
  4. category, favorites[category])
  5. else:
  6. message = "I don't have a favorite {}. I love them all!".format(category)
  7. print(message)

That works. Here's the output:

I don't have a favorite dessert. I love them all!
My favorite beverage is coffee.
My favorite vegetable is broccoli.
I don't have a favorite soup. I love them all!

Now imagine you can't modify describe() in the first place. Maybe other code relies on its original behavior, and you don't want to refactor everything. Or maybe you import it from a library, so modifying describe() isn't even an option.

You can still fix the program by catching KeyError:

  1. def describe(category):
  2. print("My favorite {} is {}.".format(
  3. category, favorites[category]))
  4. categories = ['dessert', 'beverage', 'vegetable', 'soup']
  5. for category in categories:
  6. try:
  7. describe(category)
  8. except KeyError:
  9. print("I don't have a favorite {}. I love them all!".format(category))

Same output. Use try/except KeyError when you don't have direct access to the dictionary in your code, or getting direct access is a pile of trouble.

But let's focus on working with the dict directly, because frequently you can. That brings us to the third approach: the dict.get() method. This fetches the value if the key is present, or an alternate value if not - all without raising any exception. You can call it with two arguments - the key, and the default value:

  1. >>> # Some stock prices.
  2. ... stocks = {"GNC": 8.63, "BRK.B": 170.22, "ADNT": 66.48,
  3. ... "STC": 44.82, "GK": 94.45}
  4. >>> print(stocks.get("GNC", 0.0))
  5. 8.63
  6. >>> print(stocks.get("SSL", 0.0))
  7. 0.0

You can also call it with one argument, which makes the default None.

  1. >>> print(stocks.get("GNC"))
  2. 8.63
  3. >>> print(stocks.get("SSL"))
  4. None

You might use get in a function like this:

  1. # Atomic numbers of some elements.
  2. nobles = {"He": 2, "Ne": 10,
  3. "Ar": 18, "Kr": 36, "Xe": 54}
  4. def showelement(symbol):
  5. num = nobles.get(symbol, "unknown")
  6. print("The atomic number of {} is {}".format(
  7. symbol, num))

So showelement("Kr") prints "The atomic number of Kr is 36", and showelement("Rn") prints "The atomic number of Rn is unknown". dict.get() is perfect here.

Can we use it in describe()? Yes, but that is trickier than it seems, because - unlike in showelement() - the output format is completely different if the key is not present. Here's one approach:

  1. # First version...
  2. def describe(category):
  3. fav = favorites.get(category) # default here is None
  4. if fav is None:
  5. message = "I don't have a favorite {}. I love them all!".format(category)
  6. else:
  7. message = "My favorite {} is {}.".format(category, fav)
  8. print(message)

This works... usually. The idea is to rely on the default value to signal when the key is absent. But what if the key is present, and its value is the same as the default? None can be a value in a dictionary, after all. Even if we think this dictionary won't have it, what if some bug inserts it anyway? The code above would mask that bug, lurking in your program like a land mine.

What we need is an out-of-band sentinel value - in other words, something which could not possibly be a dict value. The best way is to create a unique object - literally, an instance of object - and pass that as the default to .get():

  1. # Better version.
  2. def describe(category):
  3. # Sentinel signaling a missing key.
  4. missing = object()
  5. fav = favorites.get(category, missing)
  6. if fav is missing:
  7. message = "I don't have a favorite {}. I love them all!".format(category)
  8. else:
  9. message = "My favorite {} is {}.".format(category, fav)
  10. print(message)

missing cannot possibly be the value of any key, because we created missing just before looking in the dictionary. So if fav is missing, we know for sure it's not in the dictionary.

These three approaches - "key in dict", "try/except KeyError", and "dict.get()" - have different benefits. Practice each, so you are familiar enough to use the best for different situations.

(And, all the above is for single-threaded code. If your program has multiple threads, you have other surprises to think about. We'll cover that in another article.)

You might enjoy the Powerful Python book because it will teach you things you didn't know about Python.