Skip to content

Exceptions

What are the exceptions?

Exceptions are events that occur during program execution and interrupt its normal course. Technically, they are the objects that represent the given error.

For example, run the following code

    a = 3
    b = [1, 0, 2]
    for elem in b:
        result = a / elem
        print(f"Result is: {result}")
will cause an exception to occur ZeroDivisionError
    Result is: 3.0
    Traceback (most recent call last):
      File "program.py", line 3, in <module>
        result = a / elem
    ZeroDivisionError: division by zero

Built-in exceptions

The base exception class is Exception, all others inherit from it.

The most common exceptions:

  • AssertionError, which will occur if the condition given after the keyword assert is false:
        a = 2
        b = 1
        assert a == b
    
    Traceback (most recent call last):
      File "program.py", line 3, in <module>
        assert a == b
    AssertionError
  • AttributeError, kwhich will occur when we try to reference an attribute or method of an object that does not exist for it:
        x = 10
        x.append(8)
    
    Traceback (most recent call last):
      File "program.py", line 2, in <module>
        x.append(8)
    AttributeError: 'int' object has no attribute 'append'
  • FileNotFoundError, which signals an input/output error and occurs when you try to access a file that does not exist:
        f = open("text_file.txt")
        print(f.read())
    
    Traceback (most recent call last):
      File "program.py", line 1, in <module>
        f = open("text_file.txt")
    FileNotFoundError: [Errno 2] No such file or directory: 'text_file.txt'
  • IndexError, which occurs when trying to access a list item using index that does not exist:
        lst = [1, 2, 3, 4]
        print(lst[6])
    
    Traceback (most recent call last):
      File "program.py", line 2, in <module>
        print(lst[6])
    IndexError: list index out of range
  • ImportError, which occurs when there is an error with module import, e.g. when we try to import a function that does not exist in the module:
        from utils import calculate_diff
    
    Traceback (most recent call last):
      File "program.py", line 1, in <module>
        from utils import calculate_diff
    ImportError: cannot import name 'calculate_diff' from 'utils' (/home/utils.py)
  • ModuleNotFound, which occurs when we try to import a module that is not installed:
        import sklearn
    
    Traceback (most recent call last):
      File "program.py", line 1, in <module>
        import sklearn
    ModuleNotFoundError: No module named 'sklearn'
  • KeyError, which occurs when trying to access a dictionary item using a key that does not exist:
        people = {
            "Adam": 109,
            "Monica": 230,
            "Greogry": 1551
        }
    
        print(people["Monica"])
        print(people["Betty"])
    
    Traceback (most recent call last):
    230
      File "program.py", line 8, in <module>
        print(people["Betty"])
    KeyError: 'Betty'
  • NameError, which will show up when we try to use a variable that has not been initialized:
        a = 10
        c = a + b
        print(c)
    
    Traceback (most recent call last):
      File "program.py", line 2, in <module>
        c = a + b
    NameError: name 'b' is not defined
  • ValueError, which will occur e.g. when we pass an argument of the wrong type to a function:
        a = "Alice has a cat"
        b = int(a)
        print(a, b)
    
    Traceback (most recent call last):
      File "program.py", line 2, in <module>
        b = int(a)
    ValueError: invalid literal for int() with base 10: 'Alice has a cat'
  • ZeroDivisionError, which, as we have already seen, will occurs when you try to divide by zero.

Handling exceptions

The keywords related to exception handling are: try, except, finally, raise.

try, except

Suppose that in the event of an error in the previous program, we want to move to the next iteration cycle.

    a = 3
    b = [1, 0, 2]
    for elem in b:
        try:
            result = a / elem
        except ZeroDivisionError:
            continue
        print(f"Result is: {result}")

In this case, the try clause is executed - if no exception occurs, the except clause is skipped and we go straight to the line with the print function. However, if an exception occurs and its type matches a type defined in the except part, the code in that block is executed and the interpreter proceeds to execution of the statements following the try-except block.

Handling multiple exceptions

For each try block, we can define multiple except blocks depending on how we want to handle errors of that type.

    try:
        # the string of statements where the error may appear
    except ValueError:
        # handling an exception of ValueError type
    except (ZeroDivisionError, KeyError):
        # handling exceptions ZeroDivisionError and KeyError type
    except:
        # handling all other exceptions
However, it's worth remembering that it is a good practice to always specify the expected error after the except word - this gives you more control over your own code.

finally

The finally clause is executed regardless of whether the exception occurred or not.

    try:
        file = open("temp.txt")
        file.write("Alice has a cat")
    except IOError:
        print("An error occurred while processing the file.!")
    finally:
        file.close()

In this case, we open the file and write a sentence to it. Thanks to the use of finally, we can be sure that the file has been closed, regardless of whether there was an error.

Raising exceptions

We also have the ability to raise exceptions in the code when we want to signal that certain behavior is a bug. The keyword raise is used for this.

    a = 3
    b = [1, 0, 2]
    for elem in b:
        if elem == 0:
            raise ValueError("The divisor cannot be zero")
        result = a / elem
        print(f"Result is: {result}")

In this case, when we come across an element with a value of zero during the iteration, we want to throw a ValueError exception.

Create your own exceptions

We can also create our own exception classes, for this we need to create a class that inherits from the parent class of all exceptions: Exception.

1.

    class CustomException(Exception):
        pass


    a = 3
    b = [1, 0, 2]
    for elem in b:
        if elem == 0:
            raise CustomException("The divisor cannot be zero")
        result = a / elem
        print(f"Result is: {result}")

Using this method, we create only an empty class, and the message is given when its instance is created.

2.

    class CustomException(Exception):
        def __init__(self):
            message = "The divisor cannot be zero"
            super().__init__(message)


    a = 3
    b = [1, 0, 2]
    for elem in b:
        if elem == 0:
            raise CustomException()
        result = a / elem
        print(f"Result is: {result}")

Here, we define the message content when creating the exception class and pass it to the parent class's initialization method. Thanks to this, we do not have to do this when creating the object. This method will certainly be useful when you want to use the exception multiple times - each instance of this class will already have a message written to it that will be forwarded in case an exception is thrown.