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

AttributeError

Raised when attribute assignment or reference fails.

ImportError

Raised when an imported module is not found.

IndexError

Raised when the index of a sequence is out of range.

KeyError

Raised when a key is not found in a dictionary.

KeyboardInterrupt

Raised when the user hits the interrupt key (Ctrl+C or Delete).

MemoryError

Raised when an operation runs out of memory.

NameError

Raised when a variable is not found in local or global scope.

RuntimeError

Raised when an error does not fall under any other category.

SyntaxError

Raised by the parser when a syntax error is encountered.

IndentationError

Raised when there is incorrect indentation.

TypeError

Raised when an operation or function is applied to an object of an incorrect type.

ValueError

Raised when a function gets an argument of the correct type but with an invalid value.

ZeroDivisionError

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 tryexcept 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.")

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?

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!")

One more question….

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, the raise statement triggers the except 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 tryexceptelse/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 the account_balance, this code raises an exception by using raise InsufficientFundsException.

  • When an exception occurs, the rest of the code inside the try block is skipped.

  • The except block catches the user-defined InsufficientFundsException exception, and the statements inside the except 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:

  1. 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.

  2. Think Python

  3. GeekForGeeks