Exception Handling in Python

Introduction

Creating software is hard work. To make your software better, your application needs to keep working even when the unexpected happens. For example, let's say your application needs to pull data from some API. What happens if the API server is down and it doesn't respond as it should. Well, if you don't know what APIs are, don't worry. Check out this guide to know more about APIs.

Other common issues can include a user entering invalid input, or a user trying to open a file that doesn't even exist.

All of these cases can be handled using Python’s built-in exception handling capabilities, which are commonly referred to as the try and except statements.

In this blog, you will learn about:

  • Common exceptions
  • Handling exceptions
  • Raising exceptions
  • User-defined exceptions
  • Using the finally statement
  • Using the else statement

Common Exceptions

Python supports lots of different exceptions. Here is a list of the ones that you are likely to see when you first begin using the language:

  • Exception - The base class that all the other exceptions extend.
  • AttributeError - Raised when an attribute reference or assignment fails.
  • ImportError - Raised when an import statement fails to find the module definition.
  • ModuleNotFoundError - A subclass of ImportError which is raised by import when a module could not be located
  • IndexError - Raised when a sequence subscript is out of range.
  • KeyError - Raised when a key is not found in the dictionary.
  • KeyboardInterrupt - Raised when the user hits the interrupt key (normally Control-C or Delete)
  • NameError - Raised when a local or global name is not found.
  • OSError - Raised when a function returns a system-related error
  • RuntimeError - Raised when an error is detected that doesn’t fall in any of the other categories
  • SyntaxError - Raised when the parser encounters a syntax error
  • TypeError - Raised when an operation or function is applied to an object of inappropriate type.
  • ValueError - Raised when a built-in operation or function receives an argument that has the right type but an inappropriate value
  • ZeroDivisionError - Raised when the second argument of a division or modulo operation is zero.

For a full listing of the built-in exceptions, you can check out the Python documentation here.

Handling Exceptions

Python comes with a special syntax that you can use to catch an exception. It is known as the try/except statement. The syntax looks like this:

try:
    # Do something that may raise exception
except ExceptionName:
    # Handle the exception

In the try block, we generally put the code that may raise an exception. In the except block, we handle the exception raised in the try block. The code in the except block runs only if the name of the exception matches the ExceptionName. For example, if we try to open a file that doesn't happen to exist in the system, we get a FileNotFoundError.

with open("test.txt") as f:
    data = f.readlines()

print(data)

Output:

$ py test.py 
Traceback (most recent call last):
  File "C:\Users\ashut\Desktop\Test\hello\test.py", line 1, in <module>
    with open("test.txt") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'test.txt'

To handle this exception, we can write something like this:

try:
    with open("test.txt") as f:
        data = f.readlines()
    print(data)
except FileNotFoundError:
    print("File could not be found!!")

Output:

$ py test.py 
File could not be found!!

We can also write the except block without specifying any exception name. It is called bare exception , but it's not recommended. The reason it is bad practice to create a bare except is that you don’t know what types of exceptions you are catching, nor exactly where they are occurring. This can make figuring out what you did wrong more difficult. If you narrow the exception types down to the ones you know how to deal with, then the unexpected ones will make your application crash with a useful message. At that point, you can decide if you want to catch that other exception or not.

try:
    with open("test.txt") as f:
        data = f.readlines()
    print(data)
except:
    print("Something went wrong!")

The code would still work and output:

$ py test.py 
Something went wrong!

But in this case, we couldn't understand what exactly went wrong.

Handling Multiple Exceptions

We can even handle multiple exceptions. Suppose, we try to open a file that doesn't exist and then import an external library that isn't installed. It will raise two exceptions- FileNotFoundError and ImportError. We can handle them as:

try:
    with open("test.txt") as f:
        data = f.readlines()
    print(data)
    from fastapi import FastAPI
except FileNotFoundError:
    print("File could not be found!!")
except ImportError:
    print("Library not installed!!")

If the file test.txt is not found, it is handled by the except FileNotFoundError block and we get an error message saying File could not be found!! If the file is found and fastapi is not installed, the except ImportError block handles the next exception and outputs _Library not installed!! _This exception handler will catch only two types of exceptions: FileNotFoundError and ImportError. If another type of exception occurs, this handler won’t catch it and your code will stop.

The above code can also be rewritten as:

try:
    with open("test.txt") as f:
        data = f.readlines()
    print(data)
    from fastapi import FastAPI
except (FileNotFoundError, ImportError):
    print("Something went wrong!!")

But the above code will make it harder to debug the exact problem that has happened.

Raising Exceptions

Raising exceptions is the process of forcing exceptions to occur. We raise exceptions in special cases. Suppose, you want the program execution to be stopped with a custom message.

Python has a built-in raise statement to throw exceptions.

>>> raise FileNotFoundError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError

We can add a custom message while raising the exception.

>>> raise FileNotFoundError('I am a custom message')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: I am a custom message

User-defined Exceptions

Apart from using Python's built-in exceptions, a user can define its own custom exception. To define a custom exception, we inherit the Exception class to create a new class.

>>> class UserError(Exception):
... pass
... 
>>> raise UserError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.UserError
>>> raise UserError("I am a custom error")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.UserError: I am a custom error

Here, we have created a custom exception called UserError which inherits from the Exception class. This new exception, like other exceptions, can be raised using the raise statement with an optional error message.

To learn more about custom exceptions, let's see an example. You might have come across this Guess the Number game when you were a beginner. In this game, a user keeps on guessing a random number chosen by the program until the user gets it right. During the process, the program keeps on hinting the user whether the number is higher or lower.

class ValueTooSmallError(Exception):
    """Raised when the guessed value is too small"""
    pass

class ValueTooLargeError(Exception):
    """Raised when the guessed value is too large"""
    pass

# guess this number
number = 10

# user guesses a number until he/she gets it right
while True:
    try:
        user_guess = int(input("Enter a number: "))
        if user_guess < number:
            raise ValueTooSmallError
        elif user_guess > number:
            raise ValueTooLargeError
        break
    except ValueTooSmallError:
        print("Too low, try again!")
    except ValueTooLargeError:
        print("Too large, try again!")

print("Congratulations! You guessed it right.")

Here is a sample run of the program:

$ py test.py 
Enter a number: 17
Too large, try again!
Enter a number: 3
Too low, try again!
Enter a number: 10
Congratulations! You guessed it right.

We have created two custom exceptions ValueTooSmallError and ValueTooLargeError that inherits the built-in Exception class.

In the first example of custom exception, we hadn't added anything in the body of the exception class. But we can customize it too as per our needs. Make sure, you know Object Oriented Programming in Python before you proceed. Let's see an example:

class AgeNotValidError(Exception):
    """Exception raised for errors in the age.

    Attributes:
        age -- input age that caused the error
        message -- explanation of the error
    """

    def __init__ (self, age, message="Your age must be equal to or greater than 18 years"):
        self.age = age
        self.message = message
        super(). __init__ (self.message)

age = int(input("Enter your age: "))
if not age >= 18:
    raise AgeNotValidError(age)

Output:

$ py test.py 
Enter your age: 16
Traceback (most recent call last):
  File "C:\Users\ashut\Desktop\Test\hello\test.py", line 17, in <module>
    raise AgeNotValidError(age)
__main__.AgeNotValidError: Your age must be equal to or greater than 18 years

Here, we have overridden the constructor of the Exception class to accept our own custom arguments age and message. Then, the constructor of the parent Exception class is called manually with the self.message argument using super().

The custom self.age attribute is defined to be used later.

We can pass a custom error message too.

class AgeNotValidError(Exception):
    """Exception raised for errors in the age.

    Attributes:
        age -- input age that caused the error
        message -- explanation of the error
    """

    def __init__ (self, age, message="Your age must be equal to or greater than 18 years"):
        self.age = age
        self.message = message
        super(). __init__ (self.message)

age = int(input("Enter your age: "))
if not age >= 18:
    raise AgeNotValidError(age, message=f"Your age is {age} but should be greater than or equal to 18.")

Output:

$ py test.py 
Enter your age: 13
Traceback (most recent call last):
  File "C:\Users\ashut\Desktop\Test\hello\test.py", line 17, in <module>
    raise AgeNotValidError(age, message=f"Your age is {age} but should be greater than or equal to 18.")
__main__.AgeNotValidError: Your age is 13 but should be greater than or equal to 18.

The inherited __str__ method of the Exception class is then used to display the corresponding message when AgeNotValidError is raised.

We can also customize the __str__ method itself by overriding it.

class AgeNotValidError(Exception):
    """Exception raised for errors in the age.

    Attributes:
        age -- input age that caused the error
        message -- explanation of the error
    """

    def __init__ (self, age, message="Your age must be equal to or greater than 18 years"):
        self.age = age
        self.message = message
        super(). __init__ (self.message)

    def __str__ (self):
        return f"{self.message}\nGiven Age: {self.age}"

age = int(input("Enter your age: "))
if not age >= 18:
    raise AgeNotValidError(age)

Output:

$ py test.py 
Enter your age: 14
Traceback (most recent call last):
  File "C:\Users\ashut\Desktop\Test\hello\test.py", line 20, in <module>
    raise AgeNotValidError(age)
__main__.AgeNotValidError: Your age must be equal to or greater than 18 years
Given Age: 14

Using the finally Statement

Apart from try and except block, we can optionally add a finally block too. The finally block will always get executed even if an exception is raised in the try block. For example, you can add the code to close a file in the finally block.

try:
    f = open("hello.txt", "w")
    import fastapi
except ImportError:
    print("An ImportError occurred")
finally:
    f.close()
    print("Closed the file")

Output:

$ py test.py 
An ImportError occurred
Closed the file

In the try block of the above code, we opened a file to write in it. Then we imported a library which isn't installed in the system. It will cause ImportError, which will be handled by the except block. In the finally block, we have closed the file.

Even if we don't handle the exception, the finally block is still executed.

try:
    f = open("hello.txt", "w")
    import fastapi
finally:
    f.close()
    print("Closed the file")

Output:

$ py test.py 
Closed the file
Traceback (most recent call last):
  File "C:\Users\ashut\Desktop\Test\hello\test.py", line 3, in <module>
    import fastapi
ModuleNotFoundError: No module named 'fastapi'

We can see Closed the file is still printed.

Using the else Statement

The else statement is executed when there is no exception.

try:
    print("I am try block")
except ImportError:
    print("I am except block")
else:
    print("I am else block")

Output:

$ py test.py 
I am try block
I am else block

As no exception was caused, the try and else blocks were executed.

Let's raise an exception and then see what happens:

try:
    raise ImportError
    print("I am try block")
except ImportError:
    print("I am except block")
else:
    print("I am else block")

Output:

$ py test.py 
I am except block

You can see that now only the try and except blocks were executed. Also, the print statement in the try block was never executed because whenever an exception occurs, the statements below that line are skipped over.

Wrapping Up

In this blog, we learned how to handle exceptions in Python. Learning Exception Handling is quite important as it makes the code work in a much nicer way even when the unexpected happens.

Hope you liked the blog, thanks for reading!

15