Exception handling

Published

2023-08-02

Handling exceptions

So far, what we’ve seen is that when an exception is raised our program is terminated (or not even run to begin with in the case of a SyntaxError). However, Python allows us to handle exceptions. What this means is that when an exception is raised, a specific block of code can be executed to deal with the problem.

For this we have, minimally, a try/except compound statement. This involves creating two blocks of code: a try block and an exception handler—an except block.

The code in the try block is code where we want to guard against unhandled exceptions. A try block is followed by an except block. The except block specifies the type of exception we wish to handle, and code for handling the exception.

Input validation with try/except

Here’s an example of input validation using try/except. Let’s say we want a positive integer as input. We’ve seen how to validate input in a while loop.

while True:
    n = int(input("Please enter a positive integer: "))
    if n > 0:
        break

This ensures that if the user enters an integer that’s less than one, that they’ll be prompted again until they supply a positive integer. But what happens if the naughty user enters something that cannot be converted to an integer?

Please enter a positive integer: cheese
Traceback (most recent call last):
  File "/.../code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 2, in <module>
ValueError: invalid literal for int() with base 10: 'cheese'

Python cannot convert 'cheese' to an integer and thus a ValueError is raised.

So now what? We put the code that could result in a ValueError in a try block, and then provide an exception handler in an except block. Here’s how we’d do it.

while True:
    try:
        user_input = input("Enter a positive integer: ")
        n = int(user_input)
        if n > 0:
            break
    except ValueError:
        print(f'"{user_input}" cannot be converted to an int!')

print(f'You have entered {n}, a positive integer.')

Let’s run this code, and try a little mischief:

Enter a positive integer: negative
"negative" cannot be converted to an int!
Enter a positive integer: cheese
"cheese" cannot be converted to an int!
Enter a positive integer: -42
Enter a positive integer: 15
You have entered 15, a positive integer.

See? Now mischief (or failure to read instructions) is handled gracefully.

Getting an index with try/except

Earlier, we saw that .index() will raise a ValueError exception if the argument passed to the .index() method is not found in the underlying sequence.

>>> lst = ['apple', 'boat', 'cat', 'drama']
>>> lst.index('egg')
Traceback (most recent call last):
  File "/.../code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
ValueError: 'egg' is not in list

We can use exception handling to improve on this.

lst = ['apple', 'boat', 'cat', 'drama']
s = input('Enter a string to search for: ')
try:
    i = lst.index(s)
    print(f'The index of "{s}" in {lst} is {i}.')
except ValueError:
    print(f'"{s}" was not found in {lst}.')

If we were to enter “egg” at the prompt, this code would print:

"egg" was not found in ['apple', 'boat', 'cat', 'drama']

This brings up the age-old question of whether it’s better to check first to see if you can complete an operation without error, or better to try and then handle an exception if it occurs. Sometimes these two approaches are referred to as “look before you leap” (LBYL) and “it’s easier to ask forgiveness than it is to ask for permission” (EAFP). Python favors the latter approach.

Why is this the case? Usually, EAFP makes your code more readable, and there’s no guarantee that the programmer can anticipate and write all the necessary checks to ensure an operation will be successful.

In this example, it’s a bit of a toss up. We could write:

if s in lst:
    print(f'The index of "{s}" in {lst} is {lst.index(s)}.')
else:
    print(f'"{s}" was not found in {lst}.')

Or we could write (as we did earlier):

try:
    print(f'The index of "{s}" in {lst} is {lst.index(s)}.')
except ValueError:
    print(f'"{s}" was not found in {lst}.')

Dos and don’ts

Do:

  • Keep try blocks as small as possible.
  • Catch and handle specific exceptions.
  • Avoid catching and handling IndexError, TypeError, NameError. When these occur, it’s almost always due to a defect in programming. Catching and handling these exceptions can hide defects that should be corrected.
  • Use separate except blocks to handle different kinds of exceptions.

Don’t:

  • Write one handler for different exception types.
  • Wrap all your code in one big try block.
  • Use exception handling to hide programming errors.
  • Use bare except: or except Exception:—these are too general and might catch things you shouldn’t.

Original author: Clayton Cafiero < [given name] DOT [surname] AT uvm DOT edu >

No generative AI was used in producing this material. This was written the old-fashioned way.

This material is for free use under either the GNU Free Documentation License or the Creative Commons Attribution-ShareAlike 3.0 United States License (take your pick).