Exceptions

Introduction to Software Engineering (CSSE 1001)

Author

Paul Vrbik

Published

March 13, 2025

Exceptions

When Python parses a file it will return a syntax error when the contents of the file do not correspond to valid Python code.

Code that passes parsing then transitions to run-time where errors or exceptions can occur. These are different than syntax-errors because you cannot detect them until they trigger.

Run-time errors are bad because they abort the execution of the program and all intermediate work is lost (unless saved to a file).

Parlance

To throw/raise an error means to detect an error at runtime and react with a message.

To catch/except an error means to handle the error without aborting runtime.

Catching Errors

Today we are going to learn how to catch exceptions so that our programs can continue running, even if they encounter an error.

The common run-time exceptions are in Table 1.

Error Type – Description
AssertionError An assertion fails
IOError File does not exist
IndexError Index out of range
KeyError Key in dict does not exist
NameError Variable does not exist
TypeError Unexpected type is given to a function
ValueError Correct type, but inappropriate value
ZeroDivisionError Division by zero attempted
Table 1: Exceptions

Index Error

xs = [1,2,3]
xs[4]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

Key Error

xs = {'a': 1}
xs['b']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'b'

Type Error

"two" + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
2 + "two"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Value Error

Note that the integer casting function has the type contract:

int(xs: str) -> int

and thus int("ten") should not throw a type-error because "ten" is indeed a string.

int("ten")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'ten'

Errors in Control Flow

Why do we want to catch errors? Can’t we just avoid throwing them?

Generally speaking we shouldn’t cause errors to be thrown. Practically speaking this is not possible – especially when we are interacting with the user.

Try/Except

try:
    <code>  # This code may throw an error
except ExceptionName:
    <code>  # This code is run when the error is of kind ExceptionName
Author’s Note

Do not abuse this control structure. It should only be used as a last resort or in the user facing layer of your software.

This control structure is a try-catch in other languages.

Without try-except a division by zero would throw an error.

x = 0 
1/x 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
x = 0
try:
    1/x
except ZeroDivisionError:
    print("Please don't divide by zero...")   
Please don't divide by zero...

Note that we are not obligated to include the exception kind. We can catch any exception.

x = 0
try:
    print(y)   # this throws a name error
    1/x
except:
     print("You did something fishy...")

It is bad practice to essentially ignore all errors.

We can catch among a list of exceptions:

try:
    print(y)   # this throws a name error
    1/x
except NameError:
    print("You're using an undefined name...")
except ZeroDivisionError:
    print("You divided by zero...")

Let us write a function

def io_double() -> int:

which takes no inputs, but rather prompts the user for a number and then doubles that number.

def io_double() -> int:
    str_x = input("Number please: ")
    int_x = int(str_x)
    return 2*int_x

io_double()  
Number please: 2
4
io_double()  
Number please: two
    ...
ValueError: invalid literal for int() with base 10: 'two'
def io_double() -> int:
     while True:
         str_x = input("Number please: ")
         try:
             int_x = int(str_x)
             return 2*int_x   # unreachable if line 5 errors
         except ValueError:
             print("That wasn't a number!  Try again...")

io_double() 
Number please: two
That wasn't a number!  Try again...
Number please: 3
6

Alternatively, recursion (future topic) would work here as well.

def io_double() -> int:
    str_x = input("Number please: ")
    try:
        int_x = int(str_x)
        return 2*int_x 
    except ValueError:
        print("That wasn't a number!  Try again...")
        io_double()  # recursion

Try/Except General Framework

try:
    <code>
except ExceptionName_0:
    <code>
except ExceptionName_1:
    <code>
    .
    .
    .
except ExceptionName_k:
    <code>

Raising

Finally, if you want to raise an error (rather than catch it), do the following:

def safe_div(x: int, y: int) -> float:
    """
    Return 1 / (x-y)
    """
    if x==y:
        raise ZeroDivisionError
    return 1/(x-y)

safe_div(1, 1)   
ZeroDivisionError

Summary

We can catch errors which throw at runtime with the try-except control structure.

We should only use a try-catch when absolutely necessary.