Errors and Exceptions

While executing a python program we may encounter errors. There are 2 types of errors:

  1. Syntax Errors - When you don't follow the proper structure of the python program (Like missing a quote during initialising a string).
  2. Exceptions - Sometimes even when the syntax is correct, errors may occur when the program is run or executed. These run time errors are called Exceptions (like trying to divide by zero or file does not exist).

If Exceptions are not handled properly, the program will crash and come to a sudden & unexpected halt.

Syntax Errors


In [1]:
print('Hello)


  File "<ipython-input-1-683e48a9a7a9>", line 1
    print('Hello)
                 ^
SyntaxError: EOL while scanning string literal

Exceptions


In [2]:
1 / 0


---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-2-b710d87c980c> in <module>()
----> 1 1 / 0

ZeroDivisionError: division by zero

In [3]:
open('doesnotexistfile.txt')


---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-3-c5316aff5424> in <module>()
----> 1 open('doesnotexistfile.txt')

FileNotFoundError: [Errno 2] No such file or directory: 'doesnotexistfile.txt'

Built-in Exceptions

Python creates an Exception object whenever a runtime error occurs. There are a number of built-in exceptions.


In [7]:
print(locals()['__builtins__'])


<module 'builtins' (built-in)>

Following are some of the built-in exceptions.

  • ZeroDivisionError - Raised when you try to divide a number by zero
  • FileNotFoundError - Raised when a file required does not exist
  • SyntaxError - Raised when proper syntax is not applied
  • NameError - Raised when a variable is not found in local or global scope
  • KeyError - Raised when a key is not found in a dictionary

Handling Exceptions

Python provides 'try/except' statements to handle the exceptions. The operation which can raise exception is placed inside the 'try' statement and code that handles exception is written in the 'except' clause.


In [11]:
import sys
def divide(a,b):
    try:
        return a / b
    except: 
        print(sys.exc_info()[0])
        
divide (1,2)
divide (2,0) # This will be captured by the 'except' clause


<class 'ZeroDivisionError'>

In [12]:
# print custom error message
def divide(a,b):
    try:
        return a / b
    except: 
        print('Error occured',sys.exc_info()[0])
        
divide (1,2)
divide (2,0) # This will be captured by the 'except' clause


Error occured <class 'ZeroDivisionError'>

Catching Specific Exceptions

A try clause can have any number of except clause to capture specific exceptions and only one will be executed in case an exception occurs. We can use tuple of values to specify multiple exceptions in an exception clause


In [18]:
def divide(a,b):
    try:
        return a / b
    except (ZeroDivisionError): 
        print('Number cannot be divided by zero or non-integer')
    except:
        print('Error Occured',sys.exc_info()[0])
        
divide (1,2)
divide (2,0) # This will be captured by the 'except - zero division error' clause
divide (2,'a') # This will be captured by the generic 'except' clause


Number cannot be divided by zero or non-integer
Error Occured <class 'TypeError'>

In [19]:
def divide(a,b):
    try:
        return a / b
    except (ZeroDivisionError, TypeError): # use a tuple to capture multiple errors
        print('Number cannot be divided by zero or non-integer')
    except:
        print('Error Occured',sys.exc_info()[0])
        
divide (1,2)
divide (2,0) # This will be captured by the 'except - zero division error' clause
divide (2,'a') # This will be captured by the generic 'except' clause


Number cannot be divided by zero or non-integer
Number cannot be divided by zero or non-integer

The last except clause may omit the exception name(s), to serve as a wildcard. Use this with extreme caution, since it is easy to mask a real programming error in this way! It can also be used to print an error message and then re-raise the exception (allowing a caller to handle the exception as well):


In [20]:
import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise


OS error: [Errno 2] No such file or directory: 'myfile.txt'

The try … except statement has an optional else clause, which, when present, must follow all except clauses. It is useful for code that must be executed if the try clause does not raise an exception. For example:


In [21]:
for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()


cannot open -f
/Users/cnc/Library/Jupyter/runtime/kernel-7c3ac5b3-b13f-4801-bd7c-2495eafdfe0b.json has 12 lines

The use of the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception that wasn’t raised by the code being protected by the try … except statement.

Exception Instances

The except clause may specify a variable after the exception name. The variable is bound to an exception instance with the arguments stored in instance.args. For convenience, the exception instance defines str() so the arguments can be printed directly without having to reference .args.


In [40]:
try:
    raise Exception('1002','Custom Exception Occured')
except Exception as inst:
    print(type(inst))
    print(inst)
    print(inst.args)
    errno, errdesc = inst.args
    print('Error Number:',errno)
    print('Error Description:',errdesc)


<class 'Exception'>
('1002', 'Custom Exception Occured')
('1002', 'Custom Exception Occured')
Error Number: 1002
Error Description: Custom Exception Occured

Exception handlers don’t just handle exceptions if they occur immediately in the try clause, but also if they occur inside functions that are called (even indirectly) in the try clause. For example:


In [38]:
def func_will_fail():
    return 1 / 0

try:
    func_will_fail()
except ZeroDivisionError as err:
    print('Handling Error - ',err)


Handling Error -  division by zero

Raise Exceptions

The raise statement allows the programmer to force a specified exception to occur. For example:


In [35]:
raise NameError('Error Occured')


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-35-d3d2da6432b8> in <module>()
----> 1 raise NameError('Error Occured')

NameError: Error Occured

If you need to determine whether an exception was raised but don’t intend to handle it, a simpler form of the raise statement allows you to re-raise the exception:


In [36]:
try:
    raise NameError('Error Captured')
except NameError:
    print('Captured Exception')
    raise


Captured Exception
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-36-7df1e345bdc8> in <module>()
      1 try:
----> 2     raise NameError('Error Captured')
      3 except NameError:
      4     print('Captured Exception')
      5     raise

NameError: Error Captured

User Exceptions

Python has many built-in exceptions which forces your program to output an error when something in it goes wrong.

However, sometimes you may need to create custom exceptions that serves your purpose.

In Python, users can define such exceptions by creating a new class. This exception class has to be derived, either directly or indirectly, from Exception class. Most of the built-in exceptions are also derived form this class.


In [30]:
class CustomError(Exception):
    pass

In [31]:
raise CustomError()


---------------------------------------------------------------------------
CustomError                               Traceback (most recent call last)
<ipython-input-31-59d3a407da42> in <module>()
----> 1 raise CustomError()

CustomError: 

In [32]:
raise CustomError('Unexpected Error Occured')


---------------------------------------------------------------------------
CustomError                               Traceback (most recent call last)
<ipython-input-32-1b2beb538761> in <module>()
----> 1 raise CustomError('Unexpected Error Occured')

CustomError: Unexpected Error Occured

Here, we have created a user-defined exception called CustomError which is derived from the Exception class. This new exception can be raised, like other exceptions, using the raise statement with an optional error message.

Point to Note

When we are developing a large Python program, it is a good practice to place all the user-defined exceptions that our program raises in a separate file. Many standard modules do this. They define their exceptions separately as exceptions.py or errors.py (generally but not always).

Most exceptions are defined with names that end in “Error,” similar to the naming of the standard exceptions.


In [33]:
# define Python user-defined exceptions
class Error(Exception):
   """Base class for other exceptions"""
   pass

class ValueTooSmallError(Error):
   """Raised when the input value is too small"""
   pass

class ValueTooLargeError(Error):
   """Raised when the input value is too large"""
   pass

# our main program
# user guesses a number until he/she gets it right

# you need to guess this number
number = 10

while True:
   try:
       i_num = int(input("Enter a number: "))
       if i_num < number:
           raise ValueTooSmallError
       elif i_num > number:
           raise ValueTooLargeError
       break
   except ValueTooSmallError:
       print("This value is too small, try again!")
       print()
   except ValueTooLargeError:
       print("This value is too large, try again!")
       print()

print("Congratulations! You guessed it correctly.")


Enter a number: 5
This value is too small, try again!

Enter a number: 10
Congratulations! You guessed it correctly.

Here, we have defined a base class called Error.

The other two exceptions (ValueTooSmallError and ValueTooLargeError) that are actually raised by our program are derived from this class. This is the standard way to define user-defined exceptions in Python programming.

Many standard modules define their own exceptions to report errors that may occur in functions they define. A detailed example is given below:


In [34]:
class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

Clean up Actions

The try statement in Python can have an optional finally clause. This clause is executed no matter what, and is generally used to release external resources.

A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. When an exception has occurred in the try clause and has not been handled by an except clause (or it has occurred in an except or else clause), it is re-raised after the finally clause has been executed.

The finally clause is also executed “on the way out” when any other clause of the try statement is left via a break, continue or return statement.


In [24]:
try:
    raise KeyBoardInterrupt
finally:
    print('Bye')


Bye
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-24-9d554bc1c68d> in <module>()
      1 try:
----> 2     raise KeyBoardInterrupt
      3 finally:
      4     print('Bye')

NameError: name 'KeyBoardInterrupt' is not defined

In [26]:
def divide(a,b):
    try:
        result = a / b
    except ZeroDivisionError:
        print('Number cannot be divided by zero')
    else:
        print('Result',result)
    finally:
        print('Executed Finally Clause')

In [27]:
divide(2,1)


Result 2.0
Executed Finally Clause

In [28]:
divide(2,0)


Number cannot be divided by zero
Executed Finally Clause

In [29]:
divide('1','2')


Executed Finally Clause
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-29-fc217289e134> in <module>()
----> 1 divide('1','2')

<ipython-input-26-c2a4f470101a> in divide(a, b)
      1 def divide(a,b):
      2     try:
----> 3         result = a / b
      4     except ZeroDivisionError:
      5         print('Number cannot be divided by zero')

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

Please note that the TypeError raised by dividing two strings is not handled by the except clause and therefore re-raised after the finally clause has been executed.

Pre Clean up Actions

Some objects define standard clean-up actions to be undertaken when the object is no longer needed.

Look at the following example, which tries to open a file and print its contents to the screen.


In [ ]:
for line in open("myfile.txt"):
    print(line, end="")

The problem with this code is that it leaves the file open for an indeterminate amount of time after this part of the code has finished executing. This is not a best practice.


In [23]:
with open("test.txt") as f:
    for line in f:
        print(line, end="")


This is a test file

The with statement allows objects like files to be used in a way that ensures they are always cleaned up promptly and correctly.

After the statement is executed, the file f is always closed, even if a problem was encountered while processing the lines.