Execute Program

Python for Programmers: Handling Exceptions

Welcome to the Handling Exceptions lesson!

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

  • Most exceptions indicate some kind of problem, so their names end in "error". For example, dividing by zero raises ZeroDivisionError.

  • >
    1 / 0
    Result:
  • When an exception is raised, the containing function stops executing immediately. Then Python "unwinds" the call stack: it terminates the function that called this function, then terminates the function that called that function, etc. By default, this continues until we get to the topmost function. Then the entire program crashes with the exception.

  • We can catch an exception with a try: ... except: ... block, which stops the unwinding process. (Other languages call this try { ... } catch { ... }).

  • In the next example, the line 1 / 0 raises a ZeroDivisionError inside the try block. Python jumps to the except: block so we can handle the exception.

  • >
    try:
    1 / 0
    what_ran = "try"
    except:
    what_ran = "except"

    what_ran
    Result:
    'except'Pass Icon
  • We can use except: to recover from exceptions, for example by showing the user an error message. Other times, we use except: to catch an exception, then raise a different kind of exception. Often, the new exception adds additional details to help with debugging.

  • If we divide valid values, we don't raise an exception, so the except: block doesn't run at all.

  • >
    try:
    1 / 2
    what_ran = "try"
    except:
    what_ran = "except"

    what_ran
    Result:
    'try'Pass Icon
  • The except: above catches any kind of exception, but we usually anticipate a specific exception. For example, when indexing into a dictionary, we might anticipate a KeyError. We don't want to catch other, unexpected exceptions, because they probably indicate bugs.

  • >
    config = {
    "retry_count": 3
    }

    try:
    hostname = config["hostname"]
    except KeyError:
    hostname = "default.example.com"

    hostname
    Result:
    'default.example.com'Pass Icon
  • >
    config = {
    "retry_count": 3
    }

    try:
    retries = config["retry_count"]
    except KeyError:
    retries = 0

    retries
    Result:
    3Pass Icon
  • In the next example, we raise a ZeroDivisionError exception again. This time, we have an except KeyError:. But the exception is a ZeroDivisionError, not a KeyError, so our except: doesn't catch it. The exception escapes from the function, reaches the top level of the call stack, and the overall code example errors. It's the same result that we'd get if we hadn't wrapped the code in a try: at all.

  • >
    try:
    1 / 0
    except KeyError:
    result = "caught key error"

    result
    Result:
    ZeroDivisionError: division by zeroPass Icon
  • Exceptions form a hierarchy, which lets us handle errors at different levels of granularity. For example, ZeroDivisionError is a kind of ArithmeticError, which is a kind of Exception.

  • Here's a section of the exception hierarchy, showing how different exceptions relate. The full hierarchy is quite large, so this is just a small sample!

    • Exception
      • ArithmeticError
        • OverflowError (a number was too big)
        • ZeroDivisionError (we divided by 0)
      • LookupError
        • IndexError (a list index didn't exist)
        • KeyError (a dict key didn't exist)
  • We can use this hierarchy to control which exceptions we catch. For example, if some code raises a ZeroDivisionError, then we can catch it as a general Exception, or a more specific ArithmeticError, or the exact ZeroDivisionError. Each choice means something different. If we catch ArithmeticError, then we'll also catch OverflowError in addition to ZeroDivisionError.

  • >
    try:
    1 / 0
    except ZeroDivisionError:
    caught = True

    caught
    Result:
    TruePass Icon
  • >
    try:
    1 / 0
    except ArithmeticError:
    caught = True

    caught
    Result:
    TruePass Icon
  • >
    try:
    1 / 0
    except Exception:
    caught = True

    caught
    Result:
    TruePass Icon
  • We can have multiple except: blocks, each catching a different exception type. Python runs the first except: block that matches the exception.

  • >
    config = {
    "hostname": "api.example.com"
    }

    try:
    hostname = config["HOSTNAME"]
    except ZeroDivisionError:
    caught_error = "divide_by_zero"
    except KeyError:
    caught_error = "key"

    caught_error
    Result:
    'key'Pass Icon
  • When more than one except: matches the exception, the first one "wins", and the others don't run at all. In the next example, all three except:s match the exception, but only the first one runs.

  • >
    catches = []

    try:
    1 / 0
    except ZeroDivisionError:
    catches.append("zero division error")
    except ArithmeticError:
    catches.append("arithmetic error")
    except Exception:
    catches.append("exception")

    catches
    Result:
    ['zero division error']Pass Icon
  • We can add a finally: block, which always runs, whether an exception is raised or not. Its most common job is to clean up resources that we used during the try: block. For example, we can use it to close files or network connections.

  • In the next example, we build a list while handling exceptions. At the end of the example, we have list elements showing exactly which blocks ran.

  • >
    trace = []

    try:
    1 / 0
    trace.append("try finished")
    except ZeroDivisionError:
    trace.append("caught zero division")
    except KeyError:
    trace.append("caught key error")
    finally:
    trace.append("finally")

    trace
    Result:
    ['caught zero division', 'finally']Pass Icon
  • We can add an else: block, which only runs if no exception is raised. It's similar to how the else: on an if: only runs when the condition wasn't true. This feature doesn't exist in most other programming languages, but it's very handy in some situations.

  • We can think of else: like an except: that only runs when no exception is raised. Like except:, the else: runs before the finally:.

  • The full process for exception handling is:

    • Run the try:.
    • If an exception was raised, run the first matching except:. If no exception was raised, run the else:.
    • Run the finally:.
  • First, we'll run an example that's similar to the one above, but with an else:. An exception is raised and caught here, so the else: doesn't run.

  • >
    trace = []

    try:
    1 / 0
    trace.append("try finished")
    except ZeroDivisionError:
    trace.append("caught zero division")
    except KeyError:
    trace.append("caught key error")
    else:
    trace.append("else")
    finally:
    trace.append("finally")

    trace
    Result:
    ['caught zero division', 'finally']Pass Icon
  • Second, we'll run code that doesn't raise an exception. The try: block finishes successfully. Because of that success, the else: block runs. Then the finally: block runs, as always.

  • >
    trace = []

    try:
    1000 / 2000
    trace.append("try finished")
    except ZeroDivisionError:
    trace.append("caught zero division")
    except KeyError:
    trace.append("caught key error")
    else:
    trace.append("else")
    finally:
    trace.append("finally")

    trace
    Result:
    ['try finished', 'else', 'finally']Pass Icon
  • Here's a code problem:

    The assert_password_correct function below is already complete. It raises ValueError when the passwords don't match. It also raises TypeError when the password isn't a string.

    Now we want an additional function, password_error. It calls assert_password_correct, then tells us what, if anything, was wrong with the password. Build out password_error to catch both of the exceptions mentioned above, and return string descriptions of the problems.

    • If it catches a TypeError, return "not a string".
    • If it catches a ValueError, return "wrong password".
    • Otherwise (else:) return None.
    def assert_password_correct(password):
    if not isinstance(password, str):
    raise TypeError("Passwords must be strings")
    if password != "hunter2":
    raise ValueError("Password didn't match")
    def password_error(password):
    try:
    assert_password_correct(password)
    except TypeError:
    return "not a string"
    except ValueError:
    return "wrong password"
    else:
    return None
    assert password_error(12) == "not a string"
    assert password_error("hunter3") == "wrong password"
    assert password_error("hunter2") is None
    Goal:
    None
    Yours:
    NonePass Icon