Errors and Exceptions

Exceptions are Python’s error-handling system. You’ve probably come across exceptions as that’s how Python lets you know when you’ve done something wrong! They’re not just there to tell you off, a good error message is to tell you that the computer has got into a situation where it doesn’t know what to do and is asking you, the programmer, to help it by giving more information.

Here we will use the module convert.py and script recipe.py we created in the previous section.

Checking the value of data

When you create a function you are creating a sort of contract. In this case, you are agreeing that if you call the function ounces_to_grams, this will “take whatever value you passed as an argument, interpret it as a mass in grams and return the corresponding mass in ounces”. It might seem silly to write that out in full like that but it’s a useful exercise to help make decisions about what the function should do in potentially unexpected situations. What should happen if you passed the string "banana"? Most users of your function would not expect that and it’s more likely that it’s a mistake that they’ve made. Good functions are easily explained and predictable and avoid guessing what the user meant. How though, can we let the person who called our function that there is a problem?

Let’s look at a simple example to demonstrate how we can write some code in our function to help the person calling it understand if they make a mistake. Our function converts masses and since there’s no such thing as a negative mass we need to decide what will happen if someone passes in a negative value. At the moment it will just return a negative mass:

recipe.py
import convert

weight_in_grams = convert.ounces_to_grams(-30)

print(f"{weight_in_grams} g")
-850.485 g

We need to decide what happens in situations like this. For the purpose of this section, we want a negative mass to be impossible so we need to add in some code to check for it. The easiest place to start could be by adding an if statement and printing out an informative message:

convert.py
def ounces_to_grams(weight):
    if weight < 0:
        print("Cannot convert negative mass")
    
    new_weight = weight * 28.3495
    return new_weight
Terminal/Command Prompt
python recipe.py
Cannot convert negative mass
-850.485 g

We see the message printed out, but the function has gone ahead and still returned the negative mass. What we want is a way to have the function display the message and stop running. We could stop the function at that point by returning something, but what? If we return something like 0 or -1 to designate the failure mode, then the person calling the function could quite easily carry on without noticing, assuming that it’s a mass.

     def ounces_to_grams(weight):
        if weight < 0:
            print("Cannot convert negative mass")
            # ... at this point we want to stop running the function

        new_weight = weight * 28.3495
        return new_weight

Raising exceptions

Python solves this by allowing us to raise exceptions. An exception is an error message which the person calling the code cannot ignore which is useful as it helps prevent them write incorrect code.

We can replace our print function call with the raise statement which we follow with the type of error we are notifying the user about.

A list of all Python exception types is here. It is important to choose the correct exception type for the error you are reporting. In our case, there is a problem with the value that the user is passing into our function, so I have chosen ValueError:

convert.py
def ounces_to_grams(weight):
    if weight < 0:
        raise ValueError("Cannot convert negative mass")
    
    new_weight = weight * 28.3495
    return new_weight

Now when we run our code, it will display the error:

Terminal/Command Prompt
python recipe.py
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 5
      2 sys.path.append('../scripts')
      3 import convert_exception
----> 5 weight_in_grams = convert_exception.ounces_to_grams(-30)
      7 print(f"{weight_in_grams} g")

File ~/work/intermediate-python/intermediate-python/pages/../scripts/convert_exception.py:3, in ounces_to_grams(weight)
      1 def ounces_to_grams(weight):
      2     if weight < 0:
----> 3         raise ValueError("Cannot convert negative mass")
      5     new_weight = weight * 28.3495
      6     return new_weight

ValueError: Cannot convert negative mass

Notice that the result of the function has not been displayed. This is because Python will stop running the script once an exception is raised. This means that we cannot accidentally ignore the error and use the erroneous value.

You have almost certainly seen error message like this crop up when writing your own code. Hopefully, now that you know how to create them yourself, they have become a little less scary!

Exercise

If we give our Morse code functions a string which can’t be converted, e.g. "hi!" or "m'aidez" then we expect it to result in an error.

Add a check to the start of the encode function to raise an exception if the passed message is not valid.

At this point, don’t worry about writing a check that’s perfect, just think of one thing that you would check for and raise an exception if you see that. Start with some code that looks something like:

if ... message ...:
    raise ...

You’ll also need to choose an appropriate exception type to raise. Take a look at the Standard Library and find the exception which makes sense if an incorrect value is passed in.

We have added some code to the beginning of morse.encode to check, in the case of the passed string being an English string, that it contains no !. If it does, we raise a ValueError:

ValueError: Raised when an operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.

letter_to_morse = {
    'a':'.-', 'b':'-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
    'g':'--.', 'h':'....', 'i':'..', 'j':'.---', 'k':'-.-', 'l':'.-..', 'm':'--', 
    'n':'-.', 'o':'---', 'p':'.--.', 'q':'--.-', 'r':'.-.', 's':'...', 't':'-',
    'u':'..-', 'v':'...-', 'w':'.--', 'x':'-..-', 'y':'-.--', 'z':'--..',
    '0':'-----', '1':'.----', '2':'..---', '3':'...--', '4':'....-',
    '5':'.....', '6':'-....', '7':'--...', '8':'---..', '9':'----.', ' ':'/'
}


def encode(message):
    if "!" in message:                                            # ← new code
        raise ValueError(f"'!' is not valid in English strings")  # ←
    
    morse = []

    for letter in message:
        letter = letter.lower()
        morse_letter = letter_to_morse[letter]
        morse.append(morse_letter)

    morse_message = " ".join(morse)
    
    return morse_message


# We need to invert the dictionary. This will create a dictionary
# that can go from the morse back to the letter
morse_to_letter = {}
for letter in letter_to_morse:
    morse = letter_to_morse[letter]
    morse_to_letter[morse] = letter


def decode(message):
    english = []

    morse_letters = message.split(" ")

    for letter in morse_letters:
        english_letter = morse_to_letter[letter]
        english.append(english_letter)

    english_message = "".join(english)
    
    return english_message