Error Handling & Logging in Python

In software development, different types of errors can occur. They could be syntax errors, logical errors, or runtime errors.

Error Handling & Logging in Python

Syntax errors most probably occur during the initial development phase and are a result of incorrect syntax. Syntax errors can be caught easily when the program is compiled for execution.

Logical errors, on the other hand, are a result of improper logical implementation. An example would be a program accessing a unsorted list assuming it to be sorted. Logical errors are the most difficult ones to track.

Runtime errors are the most interesting errors which occur, if we don’t consider all the corner cases. An example would be trying to access a non-existent file.

In this tutorial, we’ll learn how to handle errors in Python and how to log the errors for a better understanding of what went wrong inside the application.

Handling Exceptions in Python

Let’s start with a simple program to add two numbers in Python. Our program takes in two parameters as input and prints the sum.
Here is a Python program to add two numbers:

def addNumbers(a, b):
    print a + b

addNumbers(5, 10)

Try running the above Python program, and you should have the sum printed.

15

While writing the above program, we didn’t really consider the fact that anything can go wrong. What if one of the parameters passed is not a number?

addNumbers('', 10)

We haven’t handled that case, hence our program would crash with the following error message:

Traceback (most recent call last):
  File "addNumber.py", line 4, in <module>
    addNumbers('', 10)
  File "addNumber.py", line 2, in addNumbers
    print a + b
TypeError: cannot concatenate 'str' and 'int' objects

We can handle the above issue by checking if the parameters passed are integers. But that won’t solve the issue. What if the code breaks down due to some other reason and causes the program to crash?
Working with a program which crashes on being encountered with an error is not a good sight. Even if an unknown error is encountered, the code should be robust enough to handle the crash gracefully and let the user know that something is wrong.

Handling Exceptions Using Try and Except

In Python, we use the try and except statements to handle exceptions. Whenever the code breaks down, an exception is thrown without crashing the program. Let’s modify the add number program to include the try and except statements.

def addNumbers(a, b):
    try:
        return a + b
    except Exception as e:
        return 'Error occurred : ' + str(e)

print addNumbers('', 10)

Python would process all code inside the try and except statement. When it encounters an error, the control is passed to the except block, skipping the code in between.

As seen in the above code, we have moved our code inside a try and except statement. Try running the program and it should throw an error message instead of crashing the program. The reason for the exception is also returned as an exception message.

The above method handles unexpected exceptions. Let’s have a look at how to handle an expected exception. Assume that we are trying to read a particular file using our Python program, but the file doesn’t exist. In this case, we’ll handle the exception and let the user know that the file doesn’t exist when it happens. Have a look at the file reading code:

try:
    try:
        with open('fname') as f:
            content = f.readlines()
    except IOError as e:
        print str(e)
except Exception as e:
    print str(e)

In the above code, we have handled the file reading inside an IOError exception handler. If the code breaks down due to unavailability of the file fname, the error would be handled inside the IOError handler.
Similar to the IOError exceptions, there are a lot more standard exceptions like Arithmetic, OverflowError, and ImportError, to name a few.

Multiple Exceptions

We can handle multiple exceptions at a time by clubbing the standard exceptions as shown:

try:
    with open('fname') as f:
        content = f.readlines()
    printb
except (IOError,NameError) as e:
    print str(e)

The above code would raise both the IOError and NameError exceptions when the program is executed.

finally Clause

Assume that we are using certain resources in our Python program. During the execution of the program, it encountered an error and only got executed halfway. In this case, the resource would be unnecessarily held up. We can clean up such resources using the finally clause. Take a look at the below code:

try:
    filePointer = open('fname','r')
    try:
        content = filePointer.readline()
    finally:
        filePointer.close()
except IOError as e:
    print str(e)

If, during the execution of the above code, an exception is raised while reading the file, the filePointer would be closed in the finally block.

Logging in Python

When something goes wrong inside an application, it becomes easier to debug if we know the source of the error. When an exception is raised, we can log the required information to track down the issue. Python provides a simple and powerful logging library. Let’s have a look at how to use logging in Python.

import logging

# initialize the log settings
logging.basicConfig(filename='app.log',level=logging.INFO)

try:
    logging.info('Trying to open the file')
    filePointer = open('appFile','r')
    try:
        logging.info('Trying to read the file content')
        content = filePointer.readline()
    finally:
        filePointer.close()
except IOError as e:
    logging.error('Error occurred ' + str(e))

As seen in the above code, first we need to import the logging Python library and then initialize the logger with the log file name and logging level. There are five logging levels: DEBUG, INFO, WARNING, ERROR, and CRITICAL. Here we have the set the logging level to INFO, hence the INFO and above logs would be logged.

Getting the Stack Trace

In the above code we had a single program file, hence it was easier to figure out where the error had occurred. But what do we do when multiple program files are involved? In such a case, getting the stack trace of the error helps in finding the source of the error. The stack trace of the exception can be logged as shown:

import logging

# initialize the log settings
logging.basicConfig(filename = 'app.log', level = logging.INFO)

try:
    filePointer = open('appFile','r')
    try:
        content = filePointer.readline()
    finally:
        filePointer.close()
except IOError as e:
    logging.exception(str(e))

If you try to run the above program, on raising an exception the following error would be logged in the log file:

ERROR:root:[Errno 2] No such file or directory: 'appFile'
Traceback (most recent call last):
  File "readFile.py", line 7, in <module>
    filePointer = open('appFile','r')
IOError: [Errno 2] No such file or directory: 'appFile'

Wrapping It Up

In this tutorial, we saw how to get started with handling errors in Python and using the logging module to log errors. We saw the usage of try, except and finally statements, which are quite useful when dealing with error handling in Python. For more detailed information, I would recommend reading the official documentation on logging. Also have a look at the documentation for handling exceptions in Python.

Do let us know your thoughts in the comments below.