Search code examples
pythonexceptionkeyerror

Why does Python throw a KeyError when accessing an unset property?


What are the rationale and implications for throwing KeyError instead of returning None when accessing an unset property on a Python dictionary?

I know that language design involves trade-offs, some of them based on practical concerns and some based primarily on philosophy. From a computer science perspective, why has Python been designed in this way?

A couple of notes to frame the question

  • I'm primarily curious to know if this was a practical decision (i.e., it gives the developer more options to recover from the error than just returning None), or if the choice was philosophical (i.e., "accessing an unset property is, in Guido's opinion, an exceptional case").
  • Python's dict.get function has this functionality, but the literal notation appears to be optimizing for the case where the program considers an unset value exceptional.
  • It's worth noting that exception handling is considered by some to be hard to reason about because it involves a control flow that is distinct from the main program
  • If Python returned None instead of raising an exception, a program could still check for the key's existence prior to accessing its value if it did want to treat unset and None values differently. But since we wouldn't have access to the exception programming flow, I'm interested to learn what we might lose?

Solution

  • It comes from Python design philosophy:

    • Errors should not be fatal. That is, user code should be able to recover from error conditions as long as the virtual machine is still functional.
    • At the same time, errors should not pass silently (These last two items naturally led to the decision to use exceptions throughout the implementation.)

    And there is a classic case, where language that do not prefer to yell are broke: None is not always means missing key (which means broken message), it may also mean optional, but unset key (which is OK). Take for example JSON:

    >>> j = json.loads('{ "key": null }')
    >>> j['key']
    >>> j['nokey']
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    KeyError: 'nokey'
    

    With None-by-default this example becames more clunky. Take, PHP, for example:

    json_decode returns null for invalid input, even though null is also a perfectly valid object for JSON to decode to—this function is completely unreliable unless you also call json_last_error every time you use it.

    From PHP: a fractal of bad design