Exception Handling in Python

Exception handling is mechanism which allows us to handle errors gracefully while the program is running instead of abruptly ending the program execution.

Runtime Errors #

Runtime errors are the errors which happens while the program is running. Note is that runtime errors do not indicate there is a problem in the structure(or syntax) of the program. When runtime errors occurs Python interpreter perfectly understands your statement but it just can't execute it. However, Syntax Errors occurs due to incorrect structure of the program. Both type of errors halts the execution of the program as soon as they are encountered and displays a error message (or traceback) explaining the probable cause of the problem.

The following are some examples of Runtime errors.

Example 1: Division by a zero

>>>
>>> 2/0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>>

Example 2: Adding string to an integer

>>>
>>> 10 + "12"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>>

Example 3: Trying to access element at invalid index

>>>
>>> list1 = [11, 3, 99, 15]
>>>
>>> list1[10]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>>

Example 4: Opening a file in read mode which doesn't exists

>>>
>>> f = open("filedoesntexists.txt", "r")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'filedoesntexists.txt'
>>>
>>>

Again note that all the above statements are syntactically valid, the only problem is that when Python tries to execute them, they got into an invalid state.

An error which occur while the program is running is known as exception. When this happens, we say Python has raised an exception or an exception is thrown. Whenever such errors happens, Python creates a special type of object which contains all the relevant information about the error just occurred. For example, it contains line number at which error occurred, error messages(remember it is called traceback) and so on. By default, these errors simply halts the execution of the program. Exception handling mechanism allows us to deal with such errors gracefully without halting the program.

try-except statement #

In Python, we use try-except statement for exception handling. Its syntax is as follows:

try:
    # try block
    # write code that might raise an exception here
    <statement_1>
    <statement_2>
except ExceptiomType:
    # except block
    # handle exception here
    <handler>

The code begins with the word try, which is a reserved keyword, followed by a colon(:). In the next line we have try block. The try block contains code that might raise an exception. After that we have except clause that starts with the word except, which is again a reserved keyword, followed by exception type and a colon. In the next line we have a except block. The except block contains code to handle the exception. As usual code inside the try and except block must be properly indented, otherwise you will get an error.

Here is how try-except statement executes:

When an exception occurs in the try block, execution of the rest of the statements in the try block is skipped. If the exception raised matches the exception type in the except clause, the corresponding handler is executed.

If the exception raised in the try block block doesn't matches with the exception type specified in the except clause the program halts with an a traceback.

On the other hand, If no exception is raised in the try block, the except clause is skipped.

Let's take an example:

python101/Chapter-19/exception_handling.py

try:
    num =  int(input("Enter a number: "))
    result = 10/num
    print("Result: ", result)    

except ZeroDivisionError:
    print("Exception Handler for ZeroDivisionError")
    print("We cant divide a number by 0")

First Run

Run the program and enter 0

Output:

Enter a number: 0
Exception Handler for ZeroDivisionError
We cant divide a number by 0

In this example, the try block in line 3 raises a ZeroDivisionError. When exception occurs Python looks for the except clause with the matching exception type. In this case, it finds one and runs the exception handler code in that block. Notice that because exception is raised in line 3, the execution of print() statement(line 4) in the try block is skipped.

Second Run:

Run the program but this time enter a string instead of a number:

Output:

Enter a number: str
Traceback (most recent call last):
File "D:/python101/exception_example.py", line 2, in <module>
num = int(input("Enter a number: "))
ValueError: invalid literal for int() with base 10: 'str'

This time our program crashes with the ValueError exception. The problem is that the built-in int() only works with strings that contains numbers only, if you pass a string containing non-numeric character it will throw ValueError exception.

>>>
>>> int("123")                 // that's fine because string only contains numbers
123
>>>
>>> int("str")                 // error can't converts characters to numbers
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'str'
>>>
>>>

As we have don't have except clause with the ValueError exception, our program crashes with ValueError exception.

Third Run:

Run the program again and this time enter a integer.

Output:

Enter a number: 4
Result:  2.5

In this case, statements in the try block executes without throwing any exception, as a result the except clause is skipped.

Handling Multiple Exceptions #

We can add as many except clause as we want to handle different types of exceptions. The general format of such a try-except statement is as follows:

try:
    # try block
    # write code that might raise an exception here
    <statement_1>
    <statement_2>
except <ExceptiomType1>:
    # except block
    # handle ExceptiomType1 here
    <handler>
except <ExceptiomType2>:
    # except block
    # handle ExceptiomType2 here
    <handler>
except <ExceptiomType2>:
    # except block
    # handle ExceptiomType3 here
    <handler>
except:
    # handle any type of exception here
    <handler>

Here is how it works:

When exception occurs, Python matches the exception raised against the every except clause sequentially. If a match is found then the handler in the corresponding except clause is executed and rest of the of the except clauses are skipped.

In case the exception does not many any except clause before the last except clause (in line 18), then the handler in the last except clause is executed. Note that the last except clause doesn't have any exception type in front of it, as a result it can catch any type of exception. Off course, this last except clause is entirely optional but we commonly use it as a last resort to catch the unexpected errors and thus prevent the program from crashing.

The following program demonstrates how to use multiple except clauses.

python101/Chapter-19/handling_muliple_types_of_exceptions.py

try:
    num1 = int(input("Enter a num1: "))
    num2 = int(input("Enter a num2: "))

    result = num1 + num2
    print("Result: ", result)

    list1 = range(0, 100)
    print("Element at index:", result, " is ", list1[result] )

except ZeroDivisionError:
    print("\nException Handler for ZeroDivisionError")
    print("We cant divide a number by 0")

except ValueError:
    print("\nException Handler for ValueError")
    print("Invalid input: Only integers are allowed")

except IndexError:
    print("\nException Handler for IndexError")
    print("Invalid index")

except:
    print("\nSome unexpected error occurred")

First run output:

Enter a num1: 10
Enter a num2: 0

Exception Handler for ZeroDivisionError
We cant divide a number by 0

Second run output:

Enter a num1: 100
Enter a num2: a13

Exception Handler for ValueError
Invalid input: Only integers are allowed

Third run output:

Enter a num1: 5000
Enter a num2: 2
Result:  2500

Exception Handler for IndexError
Invalid index

Here is another example program which asks the user to enter the filename and then prints the content of the file to the console.

python101/Chapter-19/exception_handling_while_reading_file.py

filename = input("Enter file name: ")

try:
    f = open(filename, "r")

    for line in f:
        print(line, end="")

    f.close()

except FileNotFoundError:
    print("File not found")

except PermissionError:
    print("You don't have the permission to read the file")

except:
    print("Unexpected error while reading the file")

First Run:

Run the program and specify a file a which doesn't exists.

Output:

Enter file name: file_doesnt_exists.md
File not found

Second Run:

Run the program again and this time specify a file which you don't have permission to read.

Output:

Enter file name: /etc/passwd
You don't have the permission to read the file

Third run:

This time specify a file which does exists and you have the permission to read.

Output:

Enter file name: readme.md
First Line
Second Line
Third Line
Fourth Line
Fifth Line

The else and finally clause #

A try-except statement can also have an optional else clause which only gets executed when no exception is raised. The general format of try-except statement with else clause is as follows:

try:    
    <statement_1>
    <statement_2>
except <ExceptiomType1>:    
    <handler>
except <ExceptiomType2>:    
    <handler>
else:
    # else block only gets executed
    # when no exception is raised in the try block
    <statement>
    <statement>

Here is a rewrite of the above program using else clause.

python101/Chapter-19/else_clause_demo.py

filename = input("Enter file name: ")
import os

try:
    f = open(filename, "r")

    for line in f:
        print(line, end="")

    f.close()

except FileNotFoundError:
    print("File not found")

except PermissionError:
    print("You don't have the permission to read the file")

except FileExistsError:
    print("You don't have the permission to read the file")

except:
    print("Unexpected error while reading the file")
else:
    print("Program ran without any problem")

First run:

Run the program and enter a file which doesn't exists.

Output:

Enter file name: terraform.txt
File not found

Second run:

Again run the program but this time enter a file which does exists and you have the permission to access it.

Output:

Enter file name: readme.md
First Line
Second Line
Third Line
Fourth Line
Fifth Line
Program ran without any problem

As expected statement in else clause is executed this time.

Similarly, we add have a finally clause after all except clauses. The statements under the finally clause will always execute irrespective of whether the exception is raised nor not. It's general form is as follows:

try:    
    <statement_1>
    <statement_2>
except <ExceptiomType1>:    
    <handler>
except <ExceptiomType2>:    
    <handler>
finally:
    # statements here will always
    # execute no matter what
    <statement>
    <statement>

The finally block is commonly used to define clean up actions which must be performed under any circumstance. If try-except statement has an else clause then finally clause must appear after it.

The following program shows finally clause in action.

python101/Chapter-19/finally_clause_demo.py

filename = input("Enter file name: ")
import os

try:
    f = open(filename, "r")

    for line in f:
        print(line, end="")

    f.close()

except FileNotFoundError:
    print("File not found")

except PermissionError:
    print("You don't have the permission to read the file")

except FileExistsError:
    print("You don't have the permission to read the file")

except:
    print("Unexpected error while reading the file")
else:
    print("\nProgram ran without any problem")
finally:
    print("finally clause: This will always execute")

First run:

Enter file name: little.txt
File not found
finally clause: This will always execute

Second run:

Enter file name: readme.md
First Line
Second Line
Third Line
Fourth Line
Fifth Line
Program ran without any problem
finally clause: This will always execute

Exceptions Propagation and Raising Exceptions #

In the earlier few sections we have learned how to deal with exceptions using try-except statement. In this section we will discuss who throws an exception, how to create an exception and how they propagate.

An exception is simply an object raised by a function signaling that something unexpected has happened which the function itself can't handle. A function raises exception by creating an exception object from an appropriate class and then throws the exception to the calling code using the raise keyword as follows:

raise SomeExceptionClas("Error message describing cause of error")

We can raise exceptions from our own functions by creating an instance of RuntimeError() as follows:

raise RuntimeError("Someting went very wrong")

When an exception is raised inside a function and is not caught there, it is automatically propagated to the calling function(and any function up in the stack), until it is caught by try-except statement in some calling function. If the exception reaches the main module and still not handled, the program terminates with an error message.

Let's take an example.

Suppose we are creating a function to calculate factorial of a number. As factorial is only valid for positive integers, passing data of any other type would render the function useless. We can prevent this by checking the type of argument and raising an exception if the argument is not a positive integer. Here is the complete code.

python101/Chapter-19/factorial.py

def factorial(n):
    if  not isinstance(n, int):
        raise RuntimeError("Argument must be int")

    if n < 0:
        raise RuntimeError("Argument must be >= 0")

    f = 1
    for i in range(n):
        f *= n
        n -= 1

    return f

try:
    print("Factorial of 4 is:", factorial(4))
    print("Factorial of 12 is:", factorial("12"))
except RuntimeError:
    print("Invalid Input")

Output:

Factorial of 4 is: 24
Invalid Input

Notice that when factorial() function is called with a string argument, a runtime exception is raised in line 4. As factorial() function is not handling the exception, the raised exception is propagated back to the main module where it is caught by the except clause in line 19.

Note that in the above example we have coded try-except statement outside the factorial() function but we could have easily done the same inside the factorial() function as follows.

def factorial(n):

    try:
        if not isinstance(n, int):
            raise RuntimeError("Argument must be int")

        if n < 0:
            raise RuntimeError("Argument must be >= 0")

        f = 1
        for i in range(n):
            f *= n
            n -= 1

        return f

    except RuntimeError:
        return  "Invalid Input"

print("Factorial of 4 is:", factorial(4))
print("Factorial of 12 is:", factorial("12"))

Output:

Factorial of 4 is: 24
Factorial of 12 is: Invalid Input

However, this is not recommended. Generally, the called function throws an exception to caller and it's the duty of the calling code to handle the exception. This approach allows us to to handle exceptions in different ways, for example, in one case we show an error message to the user while in other we silently log the problem. If we were handling exceptions in the called function we would have to update the function every time a new behavior is required. In addition to that, all the functions in the Python standard library also conforms to this behavior. The library function only detects the problem and raises an exception and the client decide what it needs to be done to handle those errors.

Now Let's see what happens when a exception is raised in deeply a nested function call. Recall that if a exception raised inside the function and is not caught by the function itself, it is passed to its caller. This process repeats until it is caught by some calling function down in the stack. Consider the following example.

def funcion3():
    try:

        ...
        raise SomeException()
    statement7
    except ExceptionType4:
        handler
    statement8


def funcion2():
    try:
        ...
        function3()
        statement5
    except ExceptionType3:
        handler
    statement6

def function1():
    try:
        ...
        function2()
        statement3
    except ExceptionType2:
        handler
    statement4

def main():
    try:
        ...
        function1()
        statement1
    except ExceptionType1:
        handler
     statement2

main()

Program execution starts by calling the main() function. The main() function then invokes function1(), function1() invokes function2(), finally function2() invokes function3(). Let's assume function3() can raise different types of exceptions. Now consider the following cases.

  1. If the exception is of type ExceptionType4, statement7 is skipped and the except clause in line 7 catches it. The execution of function3() proceeds as usual and statement8 is executed.

  2. If exception is of type ExceptionType3, the execution of function3() is aborted (as there is no matching except clause to handle the raised exception) and the control is transferred to the caller i.e function2() where ExceptionType3 is handled by the except clause in line 17. The statement5 is skipped and statement6 is executed.

  3. If the exception is of type ExceptionType2, function3() is aborted and the control is transferred to the function2(). As function2() doesn't have except matching except clause to catch the exception, its execution is aborted and control transferred to the function1() where the exception is caught by the exception clause in line 26. The statement3 is skipped and statement4 is executed.

  4. If the exception is of type ExceptionType1, then control is transferred to the function main()
    (as function3(), function2() and function1 doesn't have matching except clause to handle the exception) where the exception is handled by except clause in line 35. The statement1 is skipped and statement2 is executed.

  5. If the exception is of type ExceptionType0. As none of the available functions have the ability to handle this exception, the program terminates with an error message.

Accessing Exception Object #

Now we know how to handle exceptions as well throw them whenever needed. One thing we didn't yet cover is to how to access exception object thrown by the function. We can access exception object using the following form of except clause.

except ExceptionType as e

From now on, whenever except clause catches an exception of type ExceptionType it assigns the exception object to the variable e.

The following example demonstrate how to access exception object:

python101/Chapter-18/accessing_exception_object.py

def factorial(n):
    if not isinstance(n, int):
        raise RuntimeError("Argument must be int")

    if n < 0:
        raise RuntimeError("Argument must be >= 0")

    f = 1
    for i in range(n):
        f *= n
        n -= 1

    return f


try:
    print("Factorial of 4 is:", factorial(4))
    print("Factorial of 12 is:", factorial("12"))

except RuntimeError as e:
    print("Error:", e)

Output:

Factorial of 4 is: 24
Error: Argument must be int

Notice that the error message printed by exception object(line 21) is the same which we passed while creating creating RuntimeError object (line 3).

Creating Your Own Exceptions #

So far in this chapter we have been using built-it exception classes such as ZeroDivisionError, ValueError, TypeError,RuntimeErroretc. Python also allows you to create new exception classes to cater you own specific needs. TheBaseException` class is the root of all exception classes in Python. The following figure shows exception class hierarchy in Python.

exception-class-hierarchy.png

We can create our own exception class by deriving it from Exception built-in class. The following examples shows how to create a exception class and use it.

python101/Chapter-19/InvalidFactorialArgumentException.py

class InvalidFactorialArgumentException(Exception):
    def __init__(self, message):
        super().__init__()
        self.message = message

    def __str__(self):
        return self.message

python101/Chapter-19/factorialWithCustomException.py

from InvalidFactorialArgumentException import *

def factorial(n):
    if not isinstance(n, int):
        raise InvalidFactorialArgumentException("Argument must be int")

    if n < 0:
        raise InvalidFactorialArgumentException("Argument must be >= 0")

    f = 1
    for i in range(n):
        f *= n
        n -= 1

    return f


try:
    print("Factorial of 4 is:", factorial(4))
    print("Factorial of 12 is:", factorial("12"))

except InvalidFactorialArgumentException as e:
    print("Error:", e)

Output:

Factorial of 4 is: 24
Error: Argument must be int