8. Exceptions#
In this notebook, we cover the following subjects:
Logical Errors (Exceptions)
Built-in Exceptions
Exception Handling
Custom Exceptions (Optional)
# To enable type hints for lists, dicts, tuples, and sets we need to import the following:
from typing import List, Dict, Tuple, Set, Any
8.1. Logical Errors (Exceptions)#
By now we have seen many concepts in the world of Python. From creating our own variables, trough defining our functions to working with more advanced datatypes such as lists or dictionaries we have covered it all. But as you have already experienced, errors occour all the time. Therefore, it is benefical if we can write our code in a way so it is able to catch and handle errors. This is where exceptions come to the picture.
So what is an exception?
An exception is an unexpected event that occurs during program execution. For example:
divide_by_zero = 73/0
The above code causes an exception as it is not possible to divide a number by 0.
To be more precise, errors that occur at runtime (after passing the syntax test) are called exceptions or logical errors. For instance, they occur when you:
try to divide a number by zero -
ZeroDivisionError
try to import a module that does not exist -
ImportError
try to access an index that is out of range in a list or other sequence -
IndexError
try to access a key that does not exist in a dictionary -
KeyError
and so on.
When these kinds of runtime errors happen, Python creates an exception object.
If we don’t handle it properly, Python shows a traceback, which is like a report that explains what went wrong and why. Let’s take a look at how Python deals with these errors:
divide_by_zero = 73/0
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
Cell In[2], line 1
----> 1 divide_by_zero = 73/0
ZeroDivisionError: division by zero
Here, while trying to divide 73 / 0, the program throws a system exception ZeroDivisionError
8.1.1. Error VS Exception#
Errors refer to issues like compilation failures, syntax mistakes, logical flaws in the code, library conflicts, or infinite recursion, among others. These types of errors are generally outside the programmer’s control, and we shouldn’t attempt to handle them directly.
On the other hand, exceptions are conditions that can be caught and managed within the program.
This is an error:
number_1: int = 12
text_1: str = "This is an exceptional error"
result_1: any = number_1 + text_1 # Error: unsupported operand types
This is an exception:
number_2: int = 12
text_2: str = "This is an exceptional error"
try:
result_2: any = number_2 + text_2
except TypeError:
print("TypeError: This will throw an error!")
8.2. Built-in Exceptions#
Illegal operations can cause exceptions to be raised. Python has a lot of built-in exceptions that are triggered when certain errors happen.
We can view all the built-in exceptions using the built-in local()
function as follows:
print(dir(locals()['__builtins__']))
Here, locals()['__builtins__']
will return a module of built-in exceptions, functions, and attributes and dir()
will allow us to list these attributes as strings.
# try it yourself
print(dir(locals()['__builtins__']))
8.2.1. Common built-in exceptions#
Some of the common built-in exceptions in Python along with the error that cause them are listed below:
Exception |
Cause of Error |
---|---|
|
Raised when attribute assignment or reference fails. |
|
Raised when an imported module is not found. |
|
Raised when the index of a sequence is out of range. |
|
Raised when a key is not found in a dictionary. |
|
Raised when the user hits the interrupt key (Ctrl+C or Delete). |
|
Raised when an operation runs out of memory. |
|
Raised when a variable is not found in local or global scope. |
|
Raised when an error does not fall under any other category. |
|
Raised by the parser when a syntax error is encountered. |
|
Raised when there is incorrect indentation. |
|
Raised when an operation or function is applied to an object of an incorrect type. |
|
Raised when a function gets an argument of the correct type but with an invalid value. |
|
Raised when division or modulo operation is performed with zero as the divisor. |
Now that we understand what exceptions are, let’s move on to learning how to handle them.
8.3. Exception Handling#
Now that we know that exceptions can cause a program to terminate unexpectedly, it’s important to understand how to handle them.
In Python, we can manage exceptions using the try
…except
block. The general syntax works as follows:
try:
# code that may cause exception
except:
# code to run when exception occures
Here, we place the code that might generate an exception inside the try block
. Every try block
is followed by an except block
.
When an exception occurs, it is caught by the except block
.
Note
The except block
cannot be used without thetry block
.
Now let’s check a real example:
try:
numerator: int = 10
denominator: int = 0
result: float = numerator/denominator
print(result)
except:
print("Error: Denominator cannot be 0.")
In the example above, we are trying to divide a number by 0. Here, this code generates an exception.
To handle the exception, we have put the code:
result: float = numerator/denominator
inside the try block
.
Now when an exception occurs, the rest of the code inside the try block is skipped. The except block
catches the exception and statements inside the except block
are executed. If none of the statements in the try block
generates an exception, the except block
is skipped.
8.3.1. Catching Specific Exceptions#
For each try block
, there can be zero or more except blocks
. Multiple except blocks
allow us to handle each exception differently.
The argument type of each except block
indicates the type of exception that can be handled by it. For example:
try:
even_numbers = [2,4,6,8]
print(even_numbers[5])
except ZeroDivisionError # here the argument type is ZeroDivisionErrorrror:
print("Denominator cannot be 0.")
except IndexError # here the argument type is IndexErrorexError:
print("Index Out of Bound.")
Let’s break it down what is happening here.
We have created a list named even_numbers
. As we already know, the list index starts from 0, the last element of the list, for this example, is at index 3. Notice the statement:
print(even_numbers[5])
Here, we are trying to access a value to the index 5. Hence, IndexError
exception occurs.
When the IndexError
exception occurs in the try block:
The
ZeroDivisionError
exception is skipped.The set of code inside the
IndexError
exception is executed.
What about this example?
try:
number: int = int("not_a_number")
print(number)
except ValueError:
print("Error: Invalid input. Cannot convert to integer.")
except TypeError:
print("Error: Type mismatch encountered.")
Why does the ValueError
get triggered in this case, and how does the program handle this exception?
The ValueError
is triggered because the string ”not_a_number”
cannot be converted to an integer. The try…except
block catches this error and prints the message: ”Error: Invalid input. Cannot convert to integer.”
8.3.2. try
with except
and else
clauses#
In some situations, we might want to run a certain block of code if the code block inside try
runs without any errors. For these cases, you can use the optional else
keyword with the try
statement.
Let’s look at an example:
# Program to divide two numbers
try:
numerator: float = float(input("Enter the numerator: "))
denominator: float = float(input("Enter the denominator: "))
result: float = numerator / denominator
except ValueError:
print("Invalid input! Please enter numeric values.") # custom exception message
except ZeroDivisionError::
print("You cannot divide by zero!")
else:
print(f"Result of division: {result}")
So what is happening here?
The code inside the try block
attempts to get two numbers from the user and then divide the first number by the second one. There are two things that can go wrong, or in other words: there are two possible exceptions being handled:
If the user enters non-numeric values, a
ValueError
is raised, and the message"Invalid input! Please enter numeric values."
is printed.If the denominator is zero, a
ZeroDivisionError
is raised, and the message"You cannot divide by zero!"
is printed.
If no error occurs, the else block
is executed, and the result of the division is printed.
The else block
ensures that the division is performed only when no exception has been raised, allowing the program to safely handle errors before performing any calculations.
Note
Exceptions in the else clause
are not handled by the preceding except clauses
.
I believe you are starting to grasp how exceptions work, however, it is a bit annoying that we always have to know the names of the exceptions we might encounter. Well good news, to deal with this fact, we can use the raise
keyword.
8.3.3. The raise
keyword#
We can use the raise
keyword to trigger an exception intentionally. This is helpful when you want to indicate that something unexpected has occurred in your program. By raising exceptions, you can:
Stop the program at a certain point if it encounters a situation it can’t handle.
Provide a clear error message to help you understand what went wrong.
Basic structure of raise
:
raise Exception("This is an error message.")
In this example, we are creating a general exception with the message “This is an error message.”
# Try it for yourself
raise Exception("This is an error message.")
So let’s see how the raise
keyword can be utilized!
age: int = -5 # An example of invalid input
if age < 0:
raise Exception("Age cannot be negative.")
else:
print(f' I am {age} years old')
Very similar to what we have seen right? Of course, if we want to be more precise, we could have said that this is a ValueError
. Let’s see how that would look like:
age: int = -5 # An example of invalid input
if age < 0:
raise ValueError("Age cannot be negative.")
Let’s look at something more complex. This code bellow calculates the reciprocal of the inputted number.
# Remeber, the reciprocal of a number is simply 1 divided by that number, e.g. reciprocal(x) = 1/x.
try:
num: int = int(input("Enter a number: "))
if num % 2 != 0: # Check if the number is odd
raise Exception("Not an even number!") # custom error message
# If the number is even, calculate the reciprocal
reciprocal = 1 / num
print(f"Reciprocal of {num} is: {reciprocal}")
except Exception as other_error:
print(f"Error: {other_error}")
This above code is probably more powerful than you think! Let’s think about the possible things the user can input:
the user can pass an odd number
the user can pass an even number
or?
or?
What are the third and fourth options?
Maybe a bit tricky, but remember that we can also pass 0 or a string.
Let’s cover each case one-by-one:
If we pass an odd number:
Enter a number: 1
Error: Not an even number!
If we pass an even number, the reciprocal is computed and displayed.
Enter a number: 4
Reciprocal of 4 is: 0.25
If we pass 0, we get:
Error: division by zero
but wait a minute, we did not even create a case to catch a ZeroDivision
error. Or did we?
Let’s revisit this part:
except Exception as other_error:
print(f"Error: {other_error}")
This except
block is actually covering most of the errors that can occur while we run the code. Since we used the Exception
keyword we account for almost all (all exceptions that are derived from the Exception class) possible exceptions that can arise.
Lastly, if we pass a string we get:
Error: invalid literal for int() with base 10: 'apple'
.
Cool, huh? Time for one last thing…
Note
raise
isn’t just about throwing exceptions; it’s about communicating problems effectively.
8.3.4. try
with except
and finally
clauses#
There is one more noteworthy thing, we can also add a finally
clause instead of the else
clause.
The finally block
is always executed no matter whether there is an exception or not. This makes it useful for tasks like cleaning up resources (e.g., closing files, releasing memory, etc.), which must be done regardless of success or failure.
The finally block
is optional.
For each try block
, there can be only one finally block
.
Let’s see an example:
try:
numerator: float = float(input("Enter the numerator: "))
denominator: float = float(input("Enter the denominator: "))
result: float = numerator / denominator
except ZeroDivisionError:
print("You can't divide by zero!")
else:
print(f"Result: {result}")
finally:
print("Execution finished, cleaning up...")
Again, we have two options:
If an exception is raised (e.g., division by zero), the appropriate error message is displayed, but the finally block still executes.
If no exceptions are raised, the result of the division is printed, and the finally block is executed.
Notice how in both cases the code inside the finally clause is excecuted.
What about this program bellow?
import math # so we can use the sqrt method
try:
number: float = float(input("Enter a positive number: "))
if number < 0:
raise ValueError("Number must be positive!") # custom error message
result = math.sqrt(number)
except ValueError as e:
print(e)
else:
print(f"The square root of {number} is {result:.2f}")
finally:
print("Execution complete. Thank you!")
What does the above program do?
Yes exactly! It calculates the square root of a number.
One more question….
What will happen if the user enters a negative number?
If the user enters a negative number, a ValueError
will be raised, and the message ”Number must be positive!”
will be printed.
The finally block will still execute, printing ”Execution complete. Thank you!”
8.3.5. Handling exceptions outside a function#
It is also possible to handle exceptions outside the function where we defined it. The bellow example demonstrates how to use the raise
statement inside a function to signal an error when a condition is not met (e.g., an item not being found in a collection). The error is then handled using a try
and except
block outside the function. This pattern is common in Python when building robust programs that need to handle specific error cases without crashing.
In this case, the program searches for a book in a collection and raises an exception if the book is not found. The error message is displayed using the except block.
def find_book(books: Dict[str, Dict[str, Any]], book_title: str) -> Dict[str, Any]:
"""
Searches for a book in a collection by its title.
Args:
books (Dict[str, Dict[str, Any]]): A dictionary where keys are book titles (str)
and values are dictionaries containing book details.
book_title (str): The title of the book to search for.
Returns:
Dict[str, Any]: A dictionary containing the details of the book if found.
Raises:
Exception: If the book with the specified title is not found in the collection.
"""
for title, details in books.items():
if title == book_title:
return details
raise Exception("Book not found")
# Try and except outside the function
try:
books_collection = {
"1984": {"author": "George Orwell", "year": 1949},
"To Kill a Mockingbird": {"author": "Harper Lee", "year": 1960},
"The Great Gatsby": {"author": "F. Scott Fitzgerald", "year": 1925}
}
# Searching for a book that doesn't exist
find_book(books_collection, "Moby Dick")
except Exception as error:
print(error)
Book not found
So what’s happening here?
The
find_book()
function is designed to search for a book by its title in a collection of books.It returns the book details if found or raises an exception if not.
The function explicitly
raises
an exception (raise Exception("Book not found")
) when the book is not in the collection.The
try block
contains the function call, and if the book is not found, theraise
statement triggers theexcept block
.The error message
"Book not found"
is printed, ensuring the program doesn’t crash.
This approach is ideal for building user-friendly applications where users can search for specific items, and the program neatly informs them if something is missing.
8.4. Custom Exceptions (Optional)#
So far we have seen what exceptions are, what are the most common built-in exceptions and how to use the try
… except
…else
/finally
clauses.
However, sometimes we need to define exceptions tailored for our code. In these cases, it is very likely that no built-in exceptions will suffice. But don’t worry, we can write our own custom exceptions!
Buckle up and let’s see how to do that! (NOTE: creating custom exceptions requires knowledge that we have not discussed yet)
8.4.1. Defining Custom Exceptions#
As mentioned before, we can define custom exceptions. To do so, we need to create a new class that is derived from the built-in Exception class. ( we will learn about classes in Notebook 14)
Here’s the syntax to define custom exceptions,
class CustomError(Exception):
...
pass
try:
...
except CustomError:
...
Here, CustomError
is a user-defined error which inherits from the Exception class.
Lastly, let’s look at a specific example where we use a costum exception:
# Define a custom exception
class InsufficientFundsException(Exception):
"Raised when an account has insufficient funds for a transaction"
pass
# Example account balance
account_balance = 1000
try:
withdrawal_amount = float(input("Enter amount to withdraw: "))
if withdrawal_amount > account_balance:
raise InsufficientFundsException
else:
account_balance -= withdrawal_amount
print(f"Withdrawal successful! New balance: ${account_balance:.2f}")
except InsufficientFundsException:
print("Exception occurred: Insufficient funds for this transaction.")
In the above example, we have defined the custom exception InsufficientFundsException
by creating a new class that is derived from the built-in Exception
class.
Here, when
withdrawal_amount
is greater than theaccount_balance
, this code raises an exception by usingraise InsufficientFundsException
.When an exception occurs, the rest of the code inside the
try
block is skipped.The
except
block catches the user-definedInsufficientFundsException
exception, and the statements inside theexcept
block are executed, printing the message"Exception occurred: Insufficient funds for this transaction."
For more infromation about how to define and apply custom exceptions visit this site.
8.5. Exercises#
Let’s practice! Mind that each exercise is designed with multiple levels to help you progressively build your skills. Level 1 is the foundational level, designed to be straightforward so that everyone can successfully complete it. In Level 2, we step it up a notch, expecting you to use more complex concepts or combine them in new ways. Finally, in Level 3, we get closest to exam level questions, but we may use some concepts that are not covered in this notebook. However, in programming, you often encounter situations where you’re unsure how to proceed. Fortunately, you can often solve these problems by starting to work on them and figuring things out as you go. Practicing this skill is extremely helpful, so we highly recommend completing these exercises.
For each of the exercises, make sure to add a docstring
and type hints
, and do not import any libraries unless specified otherwise.
8.5.1. Exercise 1: Halloween#
Level 1: Write a function named ghostly_division()
that prompts the user to enter a number and divides 100 by that number. Handle a ZeroDivisionError
exception if the user tries to divide by zero.
If the user enters zero, print a spooky message: "Ghosts can't divide by zero!"
Otherwise, print the result of the division.
Example input:
Enter a number: 25
Enter a number: 0
Example output:
'The ghost says: 100 divided by 25 is 4.0!'
'Ghosts can't divide by zero!'
# TODO.
Level 2: Modify the program to allow the user to keep entering numbers until they decide to quit by entering ‘q’.
Handle ZeroDivisionError
and any invalid inputs.
Example input:
Enter a number to divide 100 (or q to quit): 10
Enter a number to divide 100 (or q to quit): q
Example output:
'The ghost says: 100 divided by 10 is 10.0!'
'The ghosts will haunt you no more!'
# TODO.
Level 3: Enhance the program by counting the number of successful divisions and reporting it when the user decides to quit.
Example input: you pass this in a function call.
Enter a number to divide 100 (or q to quit): 5
Enter a number to divide 100 (or q to quit): 0
Enter a number to divide 100 (or q to quit): q
Example output:
'The ghost says: 100 divided by 5 is 20.0!'
'Ghosts can't divide by zero!'
'The ghost says: You made 1 successful division(s)!'
# TODO.
8.5.2. Exercise 2: Handball#
Level 1: Create a function named ball_catch()
that prompts the user to input their catching skill level (a number between 1 and 10).
Raise a
ValueError
if the input is out of range.If the input is valid, print a message indicating that the catch was successful.
Example input:
Enter your catching skill level (1-10): 8
Enter your catching skill level (1-10): 12
Example output:
'Your skill level is 8. You caught the ball successfully!'
'Skill level must be between 1 and 10.'
# TODO.
Level 2: Modify the program to ask the user to input a number of attempts (between 1 and 5) for catching the ball.
For each attempt, randomly generate (use the random module) a skill level from 1 to 10.
Print a message for each attempt, indicating whether they caught the ball or not based on the randomly generated skill level.
Handle invalid inputs accordingly.
Example input:
Enter the number of attempts (1-5): 3
Enter the number of attempts (1-5): 7
Example output:
'Attempt 1: Your skill level is 4. You caught the ball successfully!'
'Attempt 2: Your skill level is 7. You caught the ball successfully!'
'Attempt 3: Your skill level is 2. You missed the catch!'
'Please enter a number between 1 and 5.'
# TODO.
Level 3: Extend the program to calculate the total number of successful catches and the total attempts made.
After all attempts:
print the total successful catches
and the success rate as a percentage.
Example input: you pass this in a function call.
Enter the number of attempts (1-5): 4
Example output:
'Attempt 1: Your skill level is 6. You caught the ball successfully!'
'Attempt 2: Your skill level is 8. You caught the ball successfully!'
'Attempt 3: Your skill level is 3. You missed the catch!'
'Attempt 4: Your skill level is 5. You caught the ball successfully!'
'Total successful catches: 3'
'Success rate: 75.0%'
# TODO.
8.5.3. Exercise 3: Exotic Animals#
Level 1: Write a function named exotic_animal()
that prompts the user to enter the name of an exotic animal.
Raise a
TypeError
if the input is not a string.Print a message describing the animal.
Example input:
Enter the name of an exotic animal: Jaguar
Enter the name of an exotic animal: 123
Example output:
'You encountered a wild Jaguar in the jungle!'
'The animal name must be a string.'
# TODO.
Level 2: Modify the program to allow the user to input multiple animal names until they enter 'stop'
. (but still prevent the TypeError
)
Instead of just printing a description, categorize animals into three groups: mammals, reptiles, or birds.
Ask the user to specify the category for each animal and store the counts for each category.
Example input:
Enter the name of an exotic animal (or 'stop' to finish): Parrot
What type of animal is it? (mammal/reptile/bird): bird
Enter the name of an exotic animal (or 'stop' to finish): Crocodile
What type of animal is it? (mammal/reptile/bird): reptile
Enter the name of an exotic animal (or 'stop' to finish): stop
Example output:
'You encountered a wild Parrot in the jungle! (Bird)'
'You encountered a wild Crocodile in the jungle! (Reptile)'
'What a journey! We encountered 1 bird(s) and 1 reptile(s). Thanks for exploring the jungle!'
# TODO.
Level 3: Enhance the program to require users to define a custom exception named InvalidAnimalTypeError
for handling invalid animal types.
The program should prompt the user to enter multiple exotic animal names, allowing only valid types: "mammal,"
"reptile,"
or "bird."
If the user enters an invalid type, raise the custom exception.
When the user finishes inputting animal names, print the total counts for each category.
Example input:
Enter the name of an exotic animal (or 'stop' to finish): Giraffe
What type of animal is it? (mammal/reptile/bird): mammal
Enter the name of an exotic animal (or 'stop' to finish): Eagle
What type of animal is it? (mammal/reptile/bird): bird
Enter the name of an exotic animal (or 'stop' to finish): Crocodile
What type of animal is it? (mammal/reptile/bird): fish
Enter the name of an exotic animal (or 'stop' to finish): stop
Example output:
'You encountered a wild Giraffe in the jungle! (Mammal)'
'You encountered a wild Eagle in the jungle! (Bird)'
'InvalidAnimalTypeError: fish is not a valid animal type. Please enter mammal, reptile, or bird.'
'Total animals encountered:'
'Mammals: 1'
'Reptiles: 0'
'Birds: 1'
# TODO.
Material for the VU Amsterdam course “Introduction to Python Programming” for BSc Artificial Intelligence students. These notebooks are created using the following sources:
Learning Python by Doing: This book, developed by teachers of TU/e Eindhoven and VU Amsterdam, is the main source for the course materials. Code snippets or text explanations from the book may be used in the notebooks, sometimes with slight adjustments.