Exception Handling in Python
Tags: programming, python
Published: 2021-12-19 04:56:30

---
title: Exception Handling in Python
published: true
date: 2021-12-18 18:02:20 UTC
tags: Programming, Python
canonical_url: http://iread.ga/posts/80/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](https://iread.ga/posts/77/python-api-tutorial-how-to-interact-with-web-services) 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](https://docs.python.org/3/library/exceptions.html).  
 

# **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:

```py
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.

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

print(data)
```

Output: 

```bash
$ 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:

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

```

 Output:

```bash
$ 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.

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

```

The code would still work and output:

```bash
$ 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:

```py
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:

```py
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.

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

We can add a custom message while raising the exception.

```bash
>>> 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.

```bash
>>> 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.

```py
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:

```bash
$ 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:

```py
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:

```bash
$ 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.

```py
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:

```bash
$ 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.

```py
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:

```bash
$ 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.

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

Output: 

```bash
$ 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.

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

Output:

```bash
$ 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.

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

Output:

```bash
$ 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:

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

Output:

```bash
$ 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!