indusai.co

Errors and exceptions

When something goes wrong at runtime, Python raises an exception. Unhandled, it stops the program and prints a traceback. Handled, it lets your program recover, report the problem clearly, or clean up before exiting.

What an error looks like

python
numbers = [1, 2, 3]
print(numbers[5])
output
Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(numbers[5])
IndexError: list index out of range

Read tracebacks from the bottom up: the last line names the exception and explains it, the lines above show where it happened.

try and except

python
try:
    value = int("abc")
except ValueError:
    print("That was not a whole number")
print("The program continues")

Only the code inside try is protected. If it raises the named exception, Python jumps to the except block.

Catching several exceptions

python
def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "cannot divide by zero"
    except TypeError:
        return "both values must be numbers"

print(divide(10, 2))
print(divide(10, 0))
print(divide(10, "x"))

Or in one clause: except (ZeroDivisionError, TypeError):.

Using the exception object

python
try:
    open("missing.txt")
except FileNotFoundError as e:
    print("Problem:", e)
    print("Type:", type(e).__name__)

else and finally

else runs if no exception was raised. finally runs no matter what, even if there was a return or an unhandled error. It is for cleanup.

python
def parse(text):
    try:
        n = int(text)
    except ValueError:
        print("invalid")
    else:
        print("parsed", n)
    finally:
        print("done with", repr(text))

parse("42")
parse("x")

Raising exceptions

Use raise when your code detects a problem it cannot fix.

python
def set_age(age):
    if age < 0:
        raise ValueError(f"age cannot be negative, got {age}")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(e)

Pick the built in exception that best describes the problem: ValueError for a bad value, TypeError for a wrong type, KeyError for a missing key, and so on.

Custom exceptions

Define your own when callers need to catch your specific error.

python
class InsufficientFunds(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFunds(f"need {amount}, have {balance}")
    return balance - amount

try:
    withdraw(100, 250)
except InsufficientFunds as e:
    print("Declined:", e)

Common built in exceptions

ExceptionWhen
ValueErrorRight type, unacceptable value: int("x")
TypeErrorWrong type: "a" + 1
KeyErrorMissing dictionary key
IndexErrorList index out of range
AttributeErrorObject has no such attribute or method
NameErrorVariable not defined
ZeroDivisionErrorDivision by zero
FileNotFoundErrorFile does not exist
ImportErrorModule cannot be imported

Do not catch everything

except: with no type, or except Exception:, hides bugs. Catch the specific exceptions you expect and know how to handle. Let the rest surface so you can see and fix them.

Practice

  1. Write a function safe_int(text) that returns the integer or None if the text is not a number.
  2. Write get_item(lst, i) that returns a friendly message instead of raising on a bad index.
  3. Create a TooManyStudents exception and raise it when a class list would exceed 30 names.