Lab 07 - Exceptions
Due by 11:59pm on February 6, 2025
Starter Files
Download lab07.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 doestype(e)
output? What doese
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 notably c++, use the keyword
catch
instead ofexcept
.
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 notably c++, use the keyword
throw
instead ofraise
.
Try doing Q2 through Q4
Required Questions
Q1: Getting Familiar with Try/Except
We have given you a function called exception_maker()
that always throws a TypeError.
def exception_maker():
raise TypeError
Write a function exception_handler
that uses a try/except block to call exception_maker()
and handle the error it throws/raises. Catch this error using except Exception as e
and then use “e” to reference the type of the error.
def exception_handler():
""" Write a function that uses a try-except block to handle an exception.
If an exception is thrown/raised, then print out something like:
"Exception caught! Exception type: <<put the type of the exception here>>"
"""
"*** YOUR CODE HERE ***"
When the error is caught, print something like “Exception caught! Exception type: <
Q2: In Range and an Occasional Error
Write code in the in_range1
which will check to see if n
is within the range of 1-100 (inclusive). If the number is outside of that range, in_range1
returns False.
def in_range1(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_range1(9)
True
>>> in_range1(-4)
False
>>> in_range1(103)
False
"""
"*** YOUR CODE HERE ***"
Q3: Do it again. Except better.
This question adds onto previous question. Design in_range2()
to have an identical functionality to in_range1() except that it should raise
a ValueError
when the n
is not in the range of 1-100 (inclusive).
def in_range2(n):
""" Redo in_range1, but instead of returning False, raise a ValueError
if n is outside the range of 1-100.
"""
"*** YOUR CODE HERE ***"
Now that you have two different functions to test numbers, write a main()
function. Inside this main function write code that generates 1000 different numbers between 1 and 101. Then use both methods to validate the generated number and print out a message if the number is outside of range. This might mean you test every number you generate with both in_range1
and then with in_range2
. If a number fails validation, print out a message saying what the number was and which function marked it as out of range.
Note: You will need to add a try/except block in your main function to handle the errors generated by in_range2
. If a ValueError
is raised from in_range2()
, you will print out your error message within this try/except block.
def main():
""" Write code in the main function that generates 1000
random numbers between 1 and 101 and calls both in_range1
and in_range2 function to validate the numbers generated using
both functions.
"""
"*** YOUR CODE HERE ***"
Q4: In Bounds
Imagine a 2-dimensional grid. This grid has an x dimension and a y dimension. We want to write a function that will tell us if the parameters we supply (x and y) are within the grid space. Write a function that checks x and y against the grid dimensions given. If a dimension is out of bounds, raise an IndexError
, but if the (x, y) combination is allowed, return True.
def bound_checker(x_dimension, y_dimension, x, y):
""" If given an x and a y dimension which represent the maximum values on a grid
(think of a square with dimensions x = 10, y = 12, for example),
write a function that returns True if the x and y are within the grid,
and throws an IndexError if they are out of bounds.
>>> bound_checker(10, 12, 2, 3)
True
>>> bound_checker(10, 12, 59, 3)
Traceback (most recent call last):
...
IndexError
"""
"*** YOUR CODE HERE ***"
Q5: Optional: Input Validation
Imagine we are writing a questionnaire asking about peoples’ ages (which we’ll treat as integers). Write a function that prompts the user for input (this should look something like this: input('Enter your age: ')
). This input is read in as a string, so we need to convert it to a integer! The only problem is that if the user supplied letters in their input, that will create an error in the program.
Pass the user input string to the function validate_age(). In this function, first convert the input to an integer. This will throw a ValueError if the string contains non-numerical characters. We’ll deal with that in a minute. Then check to make sure that this is a valid age. This means that the age should be between 0 and 123 (inclusive). If the age is not in this range, raise a ValueError with the message (“Age outside range!”). If no errors are generated, return the age.
Going back to handle_user_input()
, ensure you have a try/except block that will deal with any errors generated in validate_age(). If validate_age() raises a ValueError, print an error message like “Invalid Age: {user_input}. Message: {the error message}”
def validate_age(user_input):
""" Try and convert the user_input to be an integer, check to see if the age is between 0 and 123,
and return that integer. If the age is out of the accepted age range, raise a value error
with the message "Age outside range!"
"""
"*** YOUR CODE HERE ***"
def handle_user_input():
""" Prompt the user for an input of only numbers (no letters or special characters).
Then call validate_age() to change that input to an integer. Handle any errors that
might be generated using a try/except block. If you catch/except a ValueError, print out something like:
"Invalid Age: {user_input}. contains non-numerical characters!"
"""
"*** YOUR CODE HERE ***"
Submit
Submit your lab07.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 traveling 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.