Lab 09 - Exceptions

Due by 11:59pm on 2024-02-08.

Starter Files

Download lab09.zip. Inside the archive, you will find starter files for the questions in this lab.

Topics

Intro to Exceptions

When we're writing code, our programs might run into different types of errors. A function might receive a string when it was only designed to accept integers. A connection across a network may be lost or some file is not available. Python has a wide range of built-in error types, formally called exceptions, that represent different categories of errors, such as ValueError, TypeError, ZeroDivisionError, IndexError, etc. You have probably already encountered these! 😉

Try doing Q1: In Range and an Occasional Error

We can handle these errors using Python's try, except and raise keywords. We use these keywords to interrupt the program, signal some sort of error, and then return to running the program.

Try and Except

An exception can be handled with a try and except statements. This statement has two parts; the try block and the except block.

try:
<try suite>
except <exception class> as <name>:
<except suite>

where the <exception class> as <name> is optional.

The code that might error would go inside the <try suite>. If the code does error within the <try suite> and the error being created/raised matches the <exception class>, then the code within the <except suite> will be executed. Additionally, if the code does error within the <try suite>, then none of the code in the <try suite> after the line that created the error will be executed.

For example, let's say we had a simple division function.

def div(numerator, denominator):
return numerator / denominator

If div was passed 0 in as the denominator, then the code would create/raise a ZeroDivisionError and the entire program would end. To handle this we might use try and except block.

def div(numerator, denominator):
try:
return numerator / denominator
except ZeroDivisionError as e:
print(f'Invalid denominator: {denominator}. Error: {type(e)}. Error Message: {e}')
return 0

Demo the code above by calling div(10,0). What does type(e) output? What does e output?

If you want to catch any exception, you can just type except:.

try:
<try suite>
except:
<except suite>

If you want to catch any exception, but still have access to the exception's information, you can type except Exception as e:.

try:
<try suite>
except Exception as e:
<except suite>

When talking about try and except, you might hear other programmers mention catching an exception rather than mentioning except. This because other languages, most noteably c++, use the keyword catch instead of except.

Raise

In addition to errors caused by badly written code, we can purposely create exceptions using the key word raise. We call this raising an exception.

When an exception is raised, it interrupts the normal flow of the program. The exception travels up the function call(s), and if there is a try/except block, it transfers control to the try/except block to handle the exception. If there is no try/except block, it will ultimately end the program.

def div(numerator, denominator):
if denominator == 0:
raise ZeroDivisionError
return numerator / denominator

try:
result = div(10, 0)
print(result)
except ZeroDivisionError as e:
print(f"An error occurred: {type(e)}.")

When raising an exception, you can optionally provide some values to be raised with the exception and access those values later:

def div(numerator, denominator):
if denominator == 0:
raise ZeroDivisionError("Cannot divide by zero!") # <-------
return numerator / denominator

try:
result = div(10, 0)
print(result)
except ZeroDivisionError as e:
print(f"An error occurred: {type(e)}. Error Information: {e}") # <--------
def g():
raise Exception("Error Message 1", "Error Message 2")

try:
g()
except Exception as e:
print(f"An error occurred: {type(e)}. Error Information: {e}")

When talking about creating errors, you might hear other programmers mention throwing an exception rather than raiseing an exception. This because other languages, most noteably c++, use the keyword throw instead of raise.

Try doing Q2 and Q3

Required Questions

Q1: In Range and an Occasional Error

Write a function in_range that checks if n is within the range of 1-100 (inclusive) and returns False if not.

def in_range(n):
"""Write a function that checks to see if n is
within the range of 1-100 and have it return False if not
>>> in_range(9)
True
>>> in_range(-4)
False
"""

"*** YOUR CODE HERE ***"

Next, write code in the main function that generates 1000 random numbers between 1 and 101 (to simulate an infrequent error) and checks if those numbers are within range by calling the in_range function to validate the number generated. It should print out a message if the number is bad.

Q2: Do it again. Except better.

Redo the first question, but this time have in_range2() raise a ValueError when the n is not in the range of 1-100 (inclusive). Then add a try/except block in your main function. If a ValueError is raised from in_range2(), then print out that the number provided is not within range.

def in_range2(n):
"""Write a function that throws an exception if n is not between 1
and 100. It should return None otherwise"""

"*** YOUR CODE HERE ***"

Note: Make sure you raise an exception in one function and use a try/except in the other.

Q3: In Bounds

In your Grid.py from lab08, add an in_bounds() method that takes x and y coordinates and returns True if the (x, y) postion is in bounds of the grid. It should return False otherwise.

def in_bounds(self, x, y):

Then add exception throwing (or raises) to the get and set methods to prevent them for acessing parts of the grid that are out of bounds. They should call in_bounds() and raise an IndexError exception if the coordinates are out of bounds.

Submit

Submit your lab09.py file along with your Grid.py file to Gradescope to receive credit. Submissions will be in Canvas.


Additional Info

Custom Exceptions

You can define custom exception classes by inheriting from the base Exception class or its subclasses.

class CustomException(Exception):
pass

def div(numerator, denominator):
if denominator == 0:
raise CustomException("Cannot divide by zero!")
return numerator / denominator

try:
result = div(10, 0)
print(result)
except CustomException as e:
print(f"An error occurred: {type(e)}. Message: {e}")

You can also add a constructor, __str__ method, and other methods to the CustomException class. However, doing this may override already inherited methods and functionality from the Exception class.

class CustomException(Exception):

def __init__(self, error_value):
self.error_value = error_value

def __str__(self):
return(repr(self.error_value))

Exception Traveling Up Through Function Calls

Here is an example of an exception travelling up through function call(s) until it finds a try/except block or ultimately crashed the program.

def f():
raise Exception("Error Message")

def g():
f()

def h():
g()

def j():
try:
h()
except:
print("Exception caught")

j()

Replacing the last line j() with h() will show a traceback of how the exception went through the function calls.

© 2023 Brigham Young University, All Rights Reserved