Python Basics: Exceptions

Exceptions in Python act as safety nets for our code. They help us handle errors gracefully, making our programs more reliable, from SyntaxError to ZeroDivisionError and beyond.

Python Basics: Exceptions

Previously, we have learned about complex data types in Python. 👇👇👇

Python Basics: Tuples, Lists, Sets and Dictionaries
Tuples, lists, sets, and dictionaries are fundamental data structures that power our code. Let’s explore the unique traits of each: from immutability to dynamic resizing, from uniqueness enforcement to lightning-fast data retrieval.

Now it is time to take another step. Let's quickly recall our simple script that prints greeting messages.

def print_hi(name):
    greetings = f'Hi, {name}'
    for _ in range(10):
        print(greetings)

if __name__ == '__main__':
    print_hi(f'PyCharm')

In the above script, the number of times the message is printed is fixed or hardcoded to 10. We can modify it slightly, remove that hardcode, and prompt a user for the number. Therefore, we are going to use a bunch of things that we have learned so far:

  • input() function to prompt the user for the number,
  • int() function to convert the string provided by the input() function,
  • range() along with for loop as the number of iterations is known upfront,
  • type hints to increase code readability.

First of all, we create a function that prompts a user for the number, reads the number, converts it to an integer, and finally returns it.

def get_repeat_number() -> int:
    prompt: str = f'Please provide the positive integer number\n'
    return int(input(prompt))

We have created the get_repeat_number() function that returns an integer. In the body, we have defined the prompt message "Please provide the positive integer number" and passed it down into the input() function. The result from the input() function is passed directly into int() for the conversion.

💡
\n results in printing a new line on the terminal output.

Now, let's modify the print_hi() function, so we can adjust the number of times greetings message is printed.

def print_hi(name: str, times: int):
    greetings: str = f'Hi, {name}'
    for _ in range(times):
        print(greetings)

We have slightly modified the print_hi() function and added an additional times parameter to the function. The parameter is used in the range() function to adjust the number of iterations. The final piece of this puzzle is calling these functions.

if __name__ == '__main__':
    repeat: int = get_repeat_number()
    print_hi(name=f'PyCharm', times=repeat)

Now, instead of calling the print_hi() function directly, first we call the get_repeat_number() function to obtain the desired number of iterations, and then we call the print_hi() function passing with the obtained number. The full code snippet is below.

def get_repeat_number() -> int:
    prompt: str = f'Please provide the positive integer number\n'
    return int(input(prompt))


def print_hi(name: str, times: int):
    greetings: str = f'Hi, {name}'
    for _ in range(times):
        print(greetings)


if __name__ == '__main__':
    repeat: int = get_repeat_number()
    print_hi(name=f'PyCharm', times=repeat)

We are ready to run the script. We are going to pass number 10 when prompted.

❯ python3 main.py
Please provide the positive integer number
10
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm

Voila! The greetings message has been printed 10 times! But what will happen when we provide something different than a number?

Houston, We Have a Problem!

Let's try to type "python" when prompted for the number.

❯ python3 main.py
Please provide the positive integer number
python
Traceback (most recent call last):
  File "main.py", line 13, in <module>
    repeat: int = get_repeat_number()
  File "main.py", line 3, in get_repeat_number
    return int(input(prompt))
ValueError: invalid literal for int() with base 10: 'python'

The script has ended with an error. The ValueError exception was thrown! The int() function couldn't convert "python" to any integer value. So what is an exception?

An exception is an event that occurs during the execution of a program, which disrupts the normal flow of the program's instructions. Exceptions are raised when an error or unexpected condition occurs, and they allow the program to handle these exceptional situations gracefully, rather than crashing.

The simplest way to handle an exceptional case correctly is to enclose code that might throw an exception with the try-except block and provide the type of exception we want to react upon.

if __name__ == '__main__':
    try:
        an_integer: int = int(f'python')
    except ValueError as e:
        print(f'We have an exceptional situation!')
        print(e)

Let's run it!

❯ python3 main.py
We have an exceptional situation!
invalid literal for int() with base 10: 'python'

We have executed the code in the except block, i.e. we have printed a message, then have printed an exception and what is more, the script hasn’t finished abnormally as we have handled the exceptional situation. Now let's look a bit deeper at how to handle exceptions in Python.

try:
    # code that might throw an exception
except ExceptionType as name:
    # code to be executed to handle an exception 

To provide basic exception handling we can use a try block to enclose code that might raise an exception and an except block to handle the exception. However, there are also situations where we need to handle multiple exception types.

try:
    # code that might throw an exception
except ExceptionType1 as name1:
    # code to be executed to handle ExceptionType1
except ExceptionType2 as name2:
    # code to be executed to handle ExceptionType2

We can handle different exceptions separately using multiple except blocks. Additionally, we can "merge" multiple exceptions in the case when we handle them in a common fashion.

try:
    # code that might throw an exception
except (ExceptionType1, ExceptionType2) as name:
    # code to be executed to handle ExceptionType1 and/or ExceptionType2

So basically it boils down to handling multiple exceptions in a single except block.

In some situations, we may need to execute a block of code regardless of whether an exception was thrown or not. We can use the finally block for this purpose.

try:
    # code that might throw an exception
except ExceptionType as name:
    # code to be executed to handle an exception
finally:
    # code that is always executed

It is typically used for cleanup operations or actions that must be performed no matter what. The code in the finally block will execute even if there is a return statement in the try or except block, which allows us to ensure that cleanup tasks are completed.

Another way of implementing a try-except block involves the else clause.

try:
    # Code that may or may not raise an exception
except ExceptionType:
    # Exception handling code
else:
    # Code to run when no exceptions occur

The else block is executed only if no exceptions are raised in the try block. It is used for code that should run when no exceptions occur, providing an alternative path to follow when everything goes smoothly. The code in the else block is skipped if an exception is raised.

Built-in exceptions

Python has plenty of built-in exceptions to cover common corner cases.

Exception Description
SyntaxError Raised when there is a syntax error in our code.
IndentationError Raised when there is an issue with the indentation of our code, typically in relation to block structures like loops or functions.
NameError Raised when we try to access a variable or name that doesn’t exist in the current scope.
TypeError Raised when an operation or function is applied to an object of an inappropriate type.
ValueError Raised when a function receives an argument of the correct type but an inappropriate value.
ZeroDivisionError Raised when we try to divide a number by zero.
IndexError Raised when we try to access an index that is out of range for a sequence (e.g., list, tuple, string).
KeyError Raised when we try to access a dictionary key that doesn’t exist.
FileNotFoundError Raised when trying to open a file that does not exist.
ImportError Raised when an imported module cannot be found.
AttributeError Raised when we try to access an attribute or method that does not exist for an object.
RuntimeError A generic error that can be raised when something unexpected happens during runtime.

Raising an exception

When writing our own code, we can also leverage exceptions to signal erroneous situations and handle them gracefully. When a specific error condition is encountered, the raise statement is used to explicitly raise an exception. It allows us to raise custom or built-in exceptions in our code.

def divide(x: int, y: int) -> float:
    if y == 0:
        raise ZeroDivisionError(f'Division by zero is not allowed.')
    return x / y

try:
    result: float = divide(10, 0)
except ZeroDivisionError as e:
    print(f'An error occurred: {e}')

In the above code, we have defined a function that makes division. It contains the conditional statement that checks for division by zero and raises an exception when necessary.

❯ python3 main.py
An error occurred: Division by zero is not allowed.

The exception was handled gracefully and the error message was printed.

We can also create our own custom exceptions by defining a new exception class (usually inheriting from the Exception class) and then raising instances of it when needed.

class MyZeroDivisionError(Exception):
    def __init__(self, message: str):
        super().__init__(message)

def divide(x: int, y: int) -> float:
    if y == 0:
        raise MyZeroDivisionError(f'Must not divide by 0!!!')
    return x / y

try:
    result: float = divide(10, 0)
except MyZeroDivisionError as e:
    print(f'An error occurred: {e}')

In the above code, we have defined MyZeroDivisionError to signal an erroneous situation.

❯ python3 main.py
An error occurred: Must not divide by 0!!!

Yet another exception has been handled gracefully. 😂

💡
We will explore classes and inheritance in upcoming posts.

Prepare for unexpected

Now let's get back to our starting point.

def get_repeat_number() -> int:
    prompt: str = f'Please provide the positive integer number\n'
    return int(input(prompt))


def print_hi(name: str, times: int):
    greetings: str = f'Hi, {name}'
    for _ in range(times):
        print(greetings)


if __name__ == '__main__':
    repeat: int = get_repeat_number()
    print_hi(name=f'PyCharm', times=repeat)

We already know that the above code is not prepared for corner cases, so it needs fixing.

We will enclose the get_repeat_number() function with a try block as this function might raise an exception. When the exception occurs, we will print the error message and prompt the user again. Therefore we have to add the except block to print the error message. Additionally, we will enclose the whole try-except block with a while loop to achieve a retry mechanism. Finally, we will add the else clause to break the loop if the is no error.

def get_repeat_number() -> int:
    prompt: str = f'Please provide the positive integer number\n'
    return int(input(prompt))


def print_hi(name: str, times: int):
    greetings: str = f'Hi, {name}'
    for _ in range(times):
        print(greetings)


if __name__ == '__main__':
    while True:
        try:
            repeat: int = get_repeat_number()
        except ValueError:
            print(f'The positive integer number must be provided!!!')
        else:
            break
    print_hi(name=f'PyCharm', times=repeat)

We are ready to run our script!

❯ python3 main.py
Please provide the positive integer number
a
The positive integer number must be provided!!!
Please provide the positive integer number
a
The positive integer number must be provided!!!
Please provide the positive integer number
d
The positive integer number must be provided!!!
Please provide the positive integer number
v
The positive integer number must be provided!!!
Please provide the positive integer number
9
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm
Hi, PyCharm

Everything works as expected - when we don't provide an integer, an error is printed and we are prompted again, perfect!

Summary

Exceptions are events that disrupt the normal flow of a program due to errors or exceptional situations. They allow for graceful error handling and include a variety of built-in exceptions, such as SyntaxError, ValueError, and ZeroDivisionError. Exception handling is achieved using try-except blocks, with optional else and finally clauses. This mechanism helps developers write more reliable and resilient code by providing a structured way to deal with unexpected issues.

What’s next?

Subscribe to receive more posts like this directly to your email!

Python Intermediate: Classes, objects, and inheritance
Building upon these foundational concepts, classes, and objects allow us to structure our code in a more organized and modular manner. They promote code reusability and help model real-world entities in a way that’s intuitive and maintainable.

Ideas💡, questions❔❓ ? Feel free to start the discussion 🗣️🎙️!

Do you want to stretch your brain? Get ready for brain teasers!👇👇👇

Python Basics: Brain teasers
In the world of programming, the road to expertise is paved with challenges. Don’t shy away from solving programming problems; instead, embrace them as opportunities for growth.

For more posts about Python, follow the link below.👇👇👇

Exploring Python
Python is frequently utilized in creating websites and software, as well as for automating tasks, analyzing data, and visualizing information.