Tutorials
python

Exception and Error Handling in Python

Error handling increases the robustness of your code, which guards against potential failures that would cause your program to exit in an uncontrolled fashion.

Introduction

Source

Before we get into why exception handling is essential and types of built-in exceptions that Python supports, it is necessary to understand that there is a subtle difference between an error and an exception.

Errors cannot be handled, while Python exceptions can be handled at the run time. An error can be a syntax (parsing) error, while there can be many types of exceptions that could occur during the execution and are not unconditionally inoperable. An Error might indicate critical problems that a reasonable application should not try to catch, while an Exception might indicate conditions that an application should try to catch. Errors are a form of an unchecked exception and are irrecoverable like an OutOfMemoryError, which a programmer should not try to handle.

Exception handling makes your code more robust and helps prevent potential failures that would cause your program to stop in an uncontrolled manner. Imagine if you have written a code which is deployed in production and still, it terminates due to an exception, your client would not appreciate that, so it's better to handle the particular exception beforehand and avoid the chaos.

Errors can be of various types:

  • Syntax Error
  • Out of Memory Error
  • Recursion Error
  • Exceptions

Let's see them one by one.

Syntax Error

Syntax errors often called as parsing errors, are predominantly caused when the parser detects a syntactic issue in your code.

Let's take an example to understand it.

a = 8
b = 10
c = a b
  File "<ipython-input-8-3b3ffcedf995>", line 3
    c = a b
          ^
SyntaxError: invalid syntax

The above arrow indicates when the parser ran into an error while executing the code. The token preceding the arrow causes the failure. To rectify such fundamental errors, Python will do most of your job since it will print for you the file name and the line number at which the error occurred.

Out of Memory Error

Memory errors are mostly dependent on your systems RAM and are related to Heap. If you have large objects (or) referenced objects in memory, then you will see OutofMemoryError (Source). It can be caused due to various reasons:

  • Using a 32-bit Python Architecture (Maximum Memory Allocation given is very low, between 2GB - 4GB).
  • Loading a very large data file
  • Running a Machine Learning/Deep Learning model and many more.

You can handle the memory error with the help of exception handling, a fallback exception for when the interpreter entirely runs out of memory and must immediately stop the current execution. In these rare instances, Python raises an OutofMemoryError, allowing the script to somehow catch itself and break out of the memory error and recover itself.

However, since Python adopts to the memory management architecture of the C language (malloc() function), it is not certain that all processes of the script will recover — in some cases, a MemoryError will result in an unrecoverable crash. Hence, neither it is a good practice to use exception handling for such an error, nor it is advisable.

Recursion Error

It is related to stack and occurs when you call functions. As the name suggests, recursion error transpires when too many methods, one inside another is executed (one with an infinite recursion), which is limited by the size of the stack.

All your local variables and methods call associated data will be placed on the stack. For each method call, one stack frame will be created, and local as well as method call relevant data will be placed inside that stack frame. Once the method execution is completed, the stack frame will be removed.

To reproduce this error, let's define a function recursion that will be recursive, meaning it will keep calling itself as an infinite loop method call, you will see StackOverflow or a Recursion Error because the stack frame will be populated with method data for every call, but it will not be freed.

def recursion():
    return recursion()
recursion()
---------------------------------------------------------------------------

RecursionError                            Traceback (most recent call last)

<ipython-input-3-c6e0f7eb0cde> in <module>
----> 1 recursion()


<ipython-input-2-5395140f7f05> in recursion()
      1 def recursion():
----> 2     return recursion()


... last 1 frames repeated, from the frame below ...


<ipython-input-2-5395140f7f05> in recursion()
      1 def recursion():
----> 2     return recursion()


RecursionError: maximum recursion depth exceeded

Indentation Error

Indentation error is similar in spirit to the syntax error and falls under it. However, specific to the only indentation related issues in the script.

So let's take a quick example to understand an indentation error.

for i in range(10):
print('Hello world')
  File "<ipython-input-6-628f419d2da8>", line 2
    print('Hello world')
        ^
IndentationError: expected an indented block

Exceptions

Even if the syntax of a statement or expression is correct, it may still cause an error when executed. Python exceptions are errors that are detected during execution and are not unconditionally fatal: you will soon learn in the tutorial how to handle them in Python programs. An exception object is created when a Python script raises an exception. If the script explicitly doesn't handle the exception, the program will be forced to terminate abruptly.

The programs usually do not handle exceptions, and result in error messages as shown here:

Type Error

a = 2
b = 'DataCamp'
a + b
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-7-86a706a0ffdf> in <module>
      1 a = 2
      2 b = 'DataCamp'
----> 3 a + b


TypeError: unsupported operand type(s) for +: 'int' and 'str'

Zero Division Error

100 / 0
---------------------------------------------------------------------------

ZeroDivisionError                         Traceback (most recent call last)

<ipython-input-43-e9e866a10e2a> in <module>
----> 1 100 / 0


ZeroDivisionError: division by zero

There are various types of Python exceptions, and the type is printed as part of the message: the types in the above two examples are ZeroDivisionError and TypeError. Both the error strings printed as the exception type is the name of the Python's built-in exception.

The remaining part of the error line provides the details of what caused the error based on the type of exception.

Let's now look at Python's built-in exceptions.

Built-in Exceptions

Source

Before you start learning the built-in exceptions, let's just quickly revise the four main components of exception handling, as shown in this figure.

  • Try: It will run the code block in which you expect an error to occur.

  • Except: Here, you will define the type of exception you expect in the try block (built-in or custom).

  • Else: If there isn't any exception, then this block of code will be executed (consider this as a remedy or a fallback option if you expect a part of your script to produce an exception).

  • Finally: Irrespective of whether there is an exception or not, this block of code will always be executed.

In the following section of the tutorial, you will learn about the common type of exceptions and also learn to handle them with the help of exception handling.

Keyboard Interrupt Error

The KeyboardInterrupt exception is raised when you try to stop a running program by pressing ctrl+c or ctrl+z in a command line or interrupting the kernel in Jupyter Notebook. Sometimes you might not intend to interrupt a program, but by mistake, it happens, in which case using exception handling to avoid such issues can be helpful.

In the below example, if you run the cell and interrupt the kernel, the program will raise a KeyboardInterrupt exception. inp = input() Let's now handle the KeyboardInterrupt exception.

try:
    inp = input()
    print ('Press Ctrl+C or Interrupt the Kernel:')
except KeyboardInterrupt:
    print ('Caught KeyboardInterrupt')
else:
    print ('No exception occurred')
Caught KeyboardInterrupt

Standard Error

Let's learn about some of the standard errors that could usually occur while programming.

Arithmetic Error

  • Zero Division Error
  • OverFlow Error
  • Floating Point Error

All of the above exceptions fall under the Arithmetic base class and are raised for errors in arithmetic operations, as discussed here.

Zero Division

When the divisor (second argument of the division) or the denominator is zero, then the resultant raises a zero division error.

try:  
    a = 100 / 0
    print (a)
except ZeroDivisionError:  
        print ("Zero Division Exception Raised." )
else:  
    print ("Success, no error!")
Zero Division Exception Raised.

OverFlow Error

The Overflow Error is raised when the result of an arithmetic operation is out of range. OverflowError is raised for integers that are outside a required range.

try:  
    import math
    print(math.exp(1000))
except OverflowError:  
        print ("OverFlow Exception Raised.")
else:  
    print ("Success, no error!")
OverFlow Exception Raised.

Assertion Error

When an assert statement is failed, an Assertion Error is raised.

Let's take an example to understand the assertion error. Let's say you have two variables a and b, which you need to compare. To check whether a and b are equal or not, you apply an assert keyword before that, which will raise an Assertion exception when the expression will return false.

try:  
    a = 100
    b = "DataCamp"
    assert a == b
except AssertionError:  
        print ("Assertion Exception Raised.")
else:  
    print ("Success, no error!")
Assertion Exception Raised.

Attribute Error

When a non-existent attribute is referenced, and when that attribute reference or assignment fails, an attribute error is raised.

In the below example, you can observe that the Attributes class object has no attribute with the name attribute.

class Attributes(object):
    a = 2
    print (a)

try:
    object = Attributes()
    print (object.attribute)
except AttributeError:
    print ("Attribute Exception Raised.")
2
Attribute Exception Raised.

Import Error

ImportError is raised when you try to import a module that does not exist (unable to load) in its standard path or even when you make a typo in the module's name.

import nibabel
---------------------------------------------------------------------------

ModuleNotFoundError                       Traceback (most recent call last)

<ipython-input-6-9e567e3ae964> in <module>
----> 1 import nibabel


ModuleNotFoundError: No module named 'nibabel'

Lookup Error

Lookup Error acts as a base class for the exceptions that occur when a key or index used on a mapping or sequence of a list/dictionary is invalid or does not exists.

The two types of exceptions raised are:

  • IndexError
  • KeyError

Key Error

If a key you are trying to access is not found in the dictionary, a key error exception is raised.

try:  
    a = {1:'a', 2:'b', 3:'c'}  
    print (a[4])  
except LookupError:  
    print ("Key Error Exception Raised.")
else:  
    print ("Success, no error!")
Key Error Exception Raised.

Index Error

When you are trying to access an index (sequence) of a list that does not exist in that list or is out of range of that list, an index error is raised.

try:  
    a = ['a', 'b', 'c']  
    print (a[4])  
except LookupError:  
    print ("Index Error Exception Raised, list index out of range")
else:  
    print ("Success, no error!")
Index Error Exception Raised, list index out of range

Memory Error

As discussed earlier, Memory Error is raised when an operation does not get enough memory to process further.

Name Error

Name Error is raised when a local or global name is not found.

In the below example, ans variable is not defined. Hence, you will get a name error.

try:
    print (ans)
except NameError:  
    print ("NameError: name 'ans' is not defined")
else:  
    print ("Success, no error!")
NameError: name 'ans' is not defined

Runtime Error

Not Implemented Error

This section of the tutorial is derived from this Source. Runtime Error acts as a base class for the NotImplemented Error. Abstract methods in user-defined classes should raise this exception when the derived classes override the method.

class BaseClass(object):
    """Defines the interface"""
    def __init__(self):
        super(BaseClass, self).__init__()
    def do_something(self):
        """The interface, not implemented"""
        raise NotImplementedError(self.__class__.__name__ + '.do_something')

class SubClass(BaseClass):
    """Implementes the interface"""
    def do_something(self):
        """really does something"""
        print (self.__class__.__name__ + ' doing something!')

SubClass().do_something()
BaseClass().do_something()
SubClass doing something!



---------------------------------------------------------------------------

NotImplementedError                       Traceback (most recent call last)

<ipython-input-1-57792b6bc7e4> in <module>
     14
     15 SubClass().do_something()
---> 16 BaseClass().do_something()


<ipython-input-1-57792b6bc7e4> in do_something(self)
      5     def do_something(self):
      6         """The interface, not implemented"""
----> 7         raise NotImplementedError(self.__class__.__name__ + '.do_something')
      8
      9 class SubClass(BaseClass):


NotImplementedError: BaseClass.do_something

Type Error

Type Error Exception is raised when two different or unrelated types of operands or objects are combined.

In the below example, an integer and a string are added, which results in a type error.

try:
    a = 5
    b = "DataCamp"
    c = a + b
except TypeError:
    print ('TypeError Exception Raised')
else:
    print ('Success, no error!')
TypeError Exception Raised

Value Error

Value error is raised when the built-in operation or a function receives an argument that has a correct type but invalid value.

In the below example, the built-in operation float receives an argument, which is a sequence of characters (value), which is invalid for a type float.

try:
    print (float('DataCamp'))
except ValueError:
    print ('ValueError: could not convert string to float: \'DataCamp\'')
else:
    print ('Success, no error!')
ValueError: could not convert string to float: 'DataCamp'

Python Custom Exceptions

This section of the tutorial is derived from this Source.

As studied in the previous section of the tutorial, Python has many built-in exceptions that you can use in your program. Still, sometimes, you may need to create custom exceptions with custom messages to serve your purpose.

You can achieve this by creating a new class, which will be derived from the pre-defined Exception class in Python.

class UnAcceptedValueError(Exception):   
    def __init__(self, data):    
        self.data = data
    def __str__(self):
        return repr(self.data)

Total_Marks = int(input("Enter Total Marks Scored: "))
try:
    Num_of_Sections = int(input("Enter Num of Sections: "))
    if(Num_of_Sections < 1):
        raise UnAcceptedValueError("Number of Sections can't be less than 1")
except UnAcceptedValueError as e:
    print ("Received error:", e.data)
Enter Total Marks Scored: 10
Enter Num of Sections: 0
Received error: Number of Sections can't be less than 1

In the above example, as you observed that if you enter anything less than 1, a custom exception will be raised and handled. Many standard modules define their exceptions to report errors that may occur in functions they define.

Demerits of Python Exception Handling

Making use of Python exception handling has a side effect, as well. Like, programs that make use try-except blocks to handle exceptions will run slightly slower, and the size of your code will increase.

Below is an example where the timeit module of Python is being used to check the execution time of 2 different statements. In stmt1, try-except is used to handle ZeroDivisionError, while in stmt2, if statement is used as a normal check condition. Then, you execute these statements 10000 times with variable a=0. The point to note here is that the execution time of both the statements is different. You will find that stmt1, which is handling the exception, took a slightly longer time than stmt2, which is just checking the value and doing nothing if the condition is not met.

Hence, you should limit the use of Python exception handling and use it for rare cases only. For example, when you are not sure whether the input will be an integer or a float for arithmetic calculations or not sure about the existence of a file while trying to open it.

import timeit
setup="a=0"
stmt1 = '''\
try:
    b=10/a
except ZeroDivisionError:
    pass'''
stmt2 = '''\
if a!=0:
    b=10/a'''
print("time=",timeit.timeit(stmt1,setup,number=10000))
print("time=",timeit.timeit(stmt2,setup,number=10000))
time= 0.003897680000136461
time= 0.0002797570000439009

Congratulations!

Congratulations on finishing this tutorial.

As you learned, exceptional handling helps break the typical control flow of your program by providing a mechanism to decouple Python error handling and makes your code more robust.

Python exceptional handling is one of the prime factors in making your code production-ready and future proof apart from adding unit testing and object-oriented programming.

It is a powerful technique and is a concept of mere four blocks. try block looks for exceptions thrown by the code, while the except block handles those exceptions (built-in and custom).

One good exercise for you all would be to use all four components of exceptional handling and try to make your code more robust.

Please feel free to ask any questions related to this tutorial in the comments section below.

Want to leave a comment?