A Guide to Python Exception Handling

    Ini Arthur
    Share

    In this article, we’ll look at how to handle errors and exceptions in Python — a concept known as “exception handling”.

    There are usually two types of errors you’ll experience while programming in Python: syntax errors, and exceptions.

    Any error resulting from invalid syntax, indentation or programming structure is often regarded as a syntax error. When a syntax error occurs, the program crashes at the point the syntax error occurred.

    An exception is an anomaly that disrupts the normal flow of a computer program. When exceptions occur, we’re expected to handle these exceptions to ensure that our programs don’t crash abruptly.

    Exception handling is the process whereby checks are implemented in computer programs to handle errors — whether expected or not — that may occur during the execution of our programs. (Python tends to have more of a “do the thing and ask for forgiveness” style of programming than most other languages, as discussed here and here.)

    Python Exception Handling

    Python, like every other programming language, has a way of handling exceptions that occur during a program’s execution. This means that exceptions are handled gracefully: our Python program doesn’t crash. When an error occurs at runtime in a program that’s syntactically correct, Python uses the try statement and except clause to catch and handle exceptions.

    Since most of the exceptions are expected, it’s necessary to be more targeted or specific with exception handling in our Python programs. Specific exception handling makes debugging of programs easier.

    Some Standard Python Exceptions

    Python has a list of built-in exceptions used to handle different exceptions. Below are some built-in Python Exceptions.

    SN Exception Name Description
    1 Exception All user-defined exceptions should also be derived from this class.
    2 ArithmeticError The base class for those built-in exceptions that are raised for various arithmetic errors.
    3 BufferError Raised when a buffer related operation cannot be performed.
    4 LookupError The base class for the exceptions that are raised when a key or index that’s used on a mapping or sequence is invalid.
    5 AssertionError Raised when an assert statement fails.
    6 AttributeError Raised when an attribute reference or assignment fails.
    7 ImportError Raised when the import statement has troubles trying to load a module.
    8 IndexError Raised when a sequence subscript is out of range.
    9 KeyError Raised when a mapping (dictionary) key is not found in the set of existing keys.
    10 NameError Raised when a local or global name is not found.
    11 OverflowError Raised when the result of an arithmetic operation is too large to be represented.
    12 RuntimeError Raised when an error is detected that doesn’t fall in any of the other categories.
    13 StopIteration Raised by built-in function next() and an iterator’s __next__() method to signal that there are no further items produced by the iterator.
    14 SyntaxError Raised when the parser encounters a syntax error.
    15 TypeError Raised when an operation or function is applied to an object of inappropriate type.
    16 ValueError Raised when an operation or function receives an argument that has the right type but an inappropriate value.
    17 ZeroDivisionError Raised when the second argument of a division or modulo operation is zero.
    18 FileExistsError Raised when trying to create a file or directory which already exists.
    19 FileNotFoundError Raised when a file or directory is requested but doesn’t exist.

    Handling Python Exceptions with the try and except Statements

    The try and except blocks are used for exception handling in Python. The syntax can look like this:

    try:
        # some code that could cause an exception
    except: 
        # some code to execute to handle exception
    

    The try block contains a section of code that can raise an exception, while the except block houses some code that handles the exception.

    Let’s take a simple example below:

    print(3/0)
    

    The code above will generate an error message while the program terminates:

    Traceback (most recent call last):
      File "/home/ini/Dev/Tutorial/sitepoint/exception.py", line 53, in <module>
        print(3/0)
    ZeroDivisionError: division by zero
    

    The line of code that throws the exception can be handled as follows:

    try:
        print(3/0)
    except ZeroDivisionError:
        print("Cannot divide number by Zero")
    

    In the example above, we place the first print statement within the try block. The piece of code within this block will raise an exception, because dividing a number by zero has no meaning. The except block will catch the exception raised in the try block. The try and except blocks are often used together for handling exceptions in Python. Instead of the previous error message that was generated, we simply have “Cannot divide number by Zero” printed in the console.

    Multiple Python Excepts

    There are cases where two or more except blocks are used to catch different exceptions in Python. Multiple except blocks help us catch specific exceptions and handle them differently in our program:

    try:
        number = 'one'
        print(number + 1)
        print(block)
    except NameError:
        print("Name is undefined here")
    except TypeError:
        print("Can't concatenate string and int")
    

    Here’s the output of the code above:

    Can't concatenate string and int
    

    From the example above, we have two except blocks specifying the types of exceptions we want to handle: NameError and TypeError. The first print statement in the try block throws a TypeError exception. The Python interpreter checks through each except clause to find the appropriate exception class, which is handled by the second except block. “Can’t concatenate string and int” is printed in the console.

    The second print statement in the try block is skipped because an exception has occurred. However, any code after the last except clause will be executed:

    try:
        number = 'one'
        print(number + 1)
        print(block)
    except NameError:
        print("Name is undefined here")
    except TypeError:
        print("Can't concatenate string and int")
    
    for name in ['Chris', 'Kwame', 'Adwoa', 'Bolaji']:
        print(name, end=" ")
    

    Here’s the output of the code above:

    Can't concatenate string and int
    Chris Kwame Adwoa Bolaji
    

    The for loop after the try and except blocks is executed because the exception has been handled.

    A generic Python except

    We can have a generic except block to catch all exceptions in Python. The generic except block can be used alongside other specific except blocks in our program to catch unhandled exceptions. It’s logical to place the most generic except clause after all specific except blocks. This will kick in when an unhandled exception occurs. Let’s revise our previous example with a general except block coming in last:

    names = ['Chris', 'Kwame', 'Adwoa', 'Bolaji']
    try:
        print(names[6])
        number = 'one'
        print(number + 1)
        print(block)
    except NameError:
        print("Name is undefined here")
    except TypeError:
        print("Can't concatenate string and int")
    except:
        print('Sorry an error occured somewhere!')
    
    for name in names:
        print(name, end=" ")
    

    Here’s the output of the code above:

    Sorry an error occured somewhere!
    Chris Kwame Adwoa Bolaji
    

    An IndexError exception occurs, however, since it isn’t handled in any of the specified except blocks. The generic except block handles the exception. The statement in the generic block is executed and the for loop after it is also executed, with the respective output printed in the console.

    The raise statement

    Sometimes in our Python programs we might want to raise exceptions in certain conditions that don’t match our requirement using the raise keyword. The raise statement consists of the keyword itself, an exception instance, and an optional argument. Let’s take a code snippet below:

    def validate_password(password):
        if len(password) < 8:
            raise ValueError("Password characters less than 8")
        return password
    
    try:
        user_password = input('Enter a password: ')
        validate_password(user_password)
    except ValueError:
        print('Password should have more characters')
    

    The raise ValueError checks if the password meets the required length and raises the specified exception if the condition isn’t met. Stack OverFlow examines why it’s OK to raise exceptions instead of just printing to the console:

    Raising an error halts the entire program at that point (unless the exception is caught), whereas printing the message just writes something to stdout — the output might be piped to another tool, or someone might not be running your application from the command line, and the print output may never be seen.

    Raise an exception, to delegate the handling of that condition to something further up the callstack.

    The else clause

    The else clause can be added to the standard try and except blocks. It’s placed after the except clause. The else clause contains code that we want executed if the try statement doesn’t raise an exception. Let’s consider the following code:

    try:
        number = int(input('Enter a number: '))
        if number % 2 != 0:
            raise ValueError
    except ValueError:
        print("Number must be even")
    else:
        square = number ** 2
        print(square)
    

    When a user enters an even number, our code runs without raising exceptions. Then the else clause executes. We now have the square of our even number printed in the console. However, exceptions that may occur in the else clause aren’t handled by the previous except block(s).

    The finally clause

    The finally clause can be added to try and except blocks and should be used where necessary. Code in the finally clause is always executed whether an exception occurs or not. See the code snippet below:

    try:
        with open('robots.txt', 'r', encoding='UTF-8') as f:
            first_line = f.readline()
    except IOError: 
        print('File not found!')
    else:
        upper_case = first_line.upper()
        print(upper_case.index('x'))
    finally:
        print('The Python program ends here!')
    

    Here’s the output of the code above:

    The Python program ends here!
    Traceback (most recent call last):
      File "/home/ini/Dev/python/python_projects/extra.py", line 89, in <module>
        print(upper_case.index('x'))
    ValueError: substring not found
    

    In the example above, we’re attempting to read a robots.txt file in the try clause. Since there’s no exception raised, the code in the else clause is executed. An exception is raised in the else clause because the substring x wasn’t found in the variable upper_case. When there’s no except clause to handle an exception — as seen the code snippet above — the finally clause is executed first and the exception is re-raised after.

    The Python documentation explains it like so:

    An exception could occur during execution of an except or else clause. Again, the exception is re-raised after the finally clause has been executed.

    Exception Groups

    ExceptionGroup became available in Python 3.11. It provides a means of raising multiple unrelated exceptions. The preferred syntax for handling ExceptionGroup is except*. The syntax for exception groups looks like this:

    ExceptionGroup(msg, excs)
    

    When initialized, exception groups take two arguments, msg and excs:

    • msg: a descriptive message
    • excs: a sequence of exception subgroups

    Let’s create an instance of ExceptionGroup:

    eg = ExceptionGroup('group one', [NameError("name not defined"), TypeError("type mismatch")])
    

    When instantiating an exception group, the list of exception subgroups can’t be empty. We’ll raise an instance of the exception group we created earlier:

    raise eg
    

    Here’s the output of the code above:

    + Exception Group Traceback (most recent call last):
    |   File "<string>", line 10, in <module>
      | ExceptionGroup: group one (2 sub-exceptions)
      +-+---------------- 1 ----------------
        | NameError: name not defined
        +---------------- 2 ----------------
        | TypeError: type mismatch
        +------------------------------------
    

    The displayed traceback shows all exception subgroups contained in the exception group.

    As stated earlier, ExceptionGroup is better handled with the except* clause, because it can pick out each specific exception in the exception group. The generic except clause will only handle the exception group as a unit without being specific.

    See the code snippet below:

    try:
        raise ExceptionGroup('exception group', [NameError("name not defined"), TypeError("type mismatch"), ValueError("invalid input")])
    except* NameError as e:
        print("NameError handled here.")
    except* TypeError as e:
        print("TypeError handled here.")
    except* ValueError:
        print("ValueError handled here.")
    

    Here’s the output of that code:

    NameError handled here.
    TypeError handled here.
    ValueError handled here.
    

    Each except* clause handles a targeted exception subgroup in the exception group. Any subgroup that’s not handled will re-raise an exception.

    User-defined Exceptions in Python

    Built-in exceptions are great, but there may be a need for custom exceptions for our software project. Python allows us to create user-defined exceptions to suit our needs. The Python Documentation states:

    All exceptions must be instances of a class that derives from BaseException.

    Custom exceptions are derived by inheriting Python’s Exception class. The syntax for a custom exception looks like this:

    class CustomExceptionName(Exception):
        pass
    try:
        pass
    except CustomExceptionName:
        pass
    

    Let’s create a custom exception and use it in our code in the following example:

    class GreaterThanTenError(Exception):
        pass
    
    try:
        number = int(input("Enter a number: "))
        if number > 10:
            raise GreaterThanTenError
    except GreaterThanTenError:
        print("Input greater than 10")
    else:
        for i in range(number):
            print(i ** 2, end=" ")
    finally:
        print()
        print("The Python program ends here")
    

    In the example above, we create our own class with an exception name called GreaterThanTenException, which inherits from the Exception superclass. We place in it some code that may raise an exception in the try block. The except block is our exception handler. The else clause has code to be executed if no exception is thrown. And lastly, the finally clause executes no matter the outcome.

    If a user of our Python program inputs a number above 10, a GreaterThanTenError will be raised. The except clause will handle exceptions, and then the print statement in the finally clause is executed.

    Conclusion

    In this tutorial, we’ve learned the main difference between syntax errors and exceptions. We’ve also seen that a syntax error or exception disrupts the normal flow of our program.

    We’ve also learned that the try and except statements are the standard syntax for handling exceptions in Python.

    Exception handling is important when building real-world applications, because you want to detect errors and handle them appropriately. Python provides a long list of in-built exceptions that prove useful when handling exceptions.