how to handle Exceptions (Errors) in programs

preview

These are the tests I have by the end of the chapter

 1import src.exceptions
 2import unittest
 3
 4
 5class TestExceptions(unittest.TestCase):
 6
 7    def test_catching_module_not_found_error_in_tests(self):
 8        with self.assertRaises(ModuleNotFoundError):
 9            import does_not_exist
10
11    def test_catching_name_error_in_tests(self):
12        with self.assertRaises(NameError):
13            does_not_exist
14
15    def test_catching_attribute_error_in_tests(self):
16        with self.assertRaises(AttributeError):
17            src.exceptions.does_not_exist
18
19    def test_catching_type_error_in_tests(self):
20        with self.assertRaises(TypeError):
21            src.exceptions.function_name('the input')
22
23    def test_catching_index_error_in_tests(self):
24        a_list = [1, 2, 3, 'n']
25        with self.assertRaises(IndexError):
26            a_list[4]
27        with self.assertRaises(IndexError):
28            a_list[-5]
29
30    def test_catching_key_error_in_tests(self):
31        with self.assertRaises(KeyError):
32            {'key': 'value'}['does_not_exist']
33
34    def test_catching_zero_division_error_in_tests(self):
35        with self.assertRaises(ZeroDivisionError):
36            1 / 0
37
38    def test_catching_exceptions_in_tests(self):
39        with self.assertRaises(Exception):
40            raise Exception
41
42    def test_catching_exceptions_w_messages(self):
43        with self.assertRaisesRegex(
44            Exception, 'BOOM!!!'
45        ):
46            src.exceptions.raise_exception()
47
48    def test_catching_failure(self):
49        self.assertEqual(
50            src.exceptions.an_exception_handler(
51                src.exceptions.raise_exception
52            ),
53            'failed'
54        )
55
56    def test_catching_success(self):
57        self.assertEqual(
58            src.exceptions.an_exception_handler(
59                src.exceptions.does_not_raise_exception
60            ),
61            'succeeded'
62        )
63
64
65# Exceptions seen
66# AssertionError
67# ModuleNotFoundError
68# NameError
69# AttributeError
70# TypeError
71# IndexError
72# KeyError
73# ZeroDivisionError

questions about handling Exceptions

Here are questions you can answer after going through this chapter


requirements

how to test that an Exception is raised

open the project

  • I change directory to the exceptions folder

    cd exceptions
    

    the terminal shows I am in the exceptions folder

    .../pumping_python/exceptions
    
  • I activate the virtual environment

    source .venv/bin/activate
    

    Attention

    on Windows without Windows Subsystem for Linux use .venv/bin/activate.ps1 NOT source .venv/bin/activate

    .venv/scripts/activate.ps1
    

    the terminal shows

    (.venv) .../pumping_python/exceptions
    
  • I use pytest-watch to run the tests

    pytest-watch
    

    the terminal shows

    rootdir: .../pumping_python/exceptions
    collected 8 items
    
    tests/test_exceptions.py ....                                        [100%]
    
    ============================ 8 passed in X.YZs =============================
    
  • I hold ctrl on the keyboard and click on tests/test_exceptions.py to open it in the editor


test_catching_exceptions_w_messages

I can use the assertRaisesRegex method to test the message that is included with an Exception, this helps me to tell the difference when I have two Exceptions that have the same name

  • I add a failing test to test_exceptions.py

    38    def test_catching_exceptions_in_tests(self):
    39        with self.assertRaises(Exception):
    40            raise Exception
    41
    42    def test_catching_exceptions_w_messages(self):
    43        src.exceptions.raise_exception()
    

    the terminal shows AttributeError

    AttributeError: module 'src.exceptions' has no attribute 'raise_exception'
    

GREEN: make it pass


  • I add the name to exceptions.py

    1def function_name():
    2    return None
    3
    4
    5raise_exception
    

    the terminal shows NameError

    NameError: name 'raise_exception' is not defined
    
  • I point it to None to define it

    5raise_exception = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    
  • when I make raise_exception a function

    5def raise_exception():
    6    return None
    

    the test passes

  • I want the function to raise Exception when it is called, I add assertRaises to the test in test_exceptions.py

    42    def test_catching_exceptions_w_messages(self):
    43        with self.assertRaises(Exception):
    44            src.exceptions.raise_exception()
    

    the terminal shows AssertionError

    AssertionError: Exception not raised
    
  • I add a raise statement to the raise_exception function in exceptions.py

    5def raise_exception():
    6    raise Exception
    

    the test passes

  • I can be more specific when testing for an Exception, I add assertRaisesRegex in test_exceptions.py

    42    def test_catching_exceptions_w_messages(self):
    43        with self.assertRaisesRegex(
    44            Exception, 'BOOM!!!'
    45        ):
    46            src.exceptions.raise_exception()
    

    the terminal shows AssertionError

    AssertionError: "BOOM!!!" does not match ""
    

    the assertRaisesRegex method checks that the code in its context raises the Exception it is given, with the message it is given. The default message of the Exception is the empty string ('') and the test expects "BOOM!!!"

  • the Exception is right, the message is not, I add the expected message in exceptions.py

    5def raise_exception():
    6    raise Exception('BOOM!!!')
    

    the test passes. Time to add an Exception to the program


test_catching_failure


RED: make it fail


I add a new failing test in test_exceptions.py

42      def test_catching_exceptions_w_messages(self):
43          with self.assertRaisesRegex(
44              Exception, 'BOOM!!!'
45          ):
46              src.exceptions.raise_exception()
47
48      def test_catching_failure(self):
49          self.assertEqual(
50              src.exceptions.an_exception_handler(
51                  src.exceptions.raise_exception
52              ),
53              'failed'
54          )

the terminal shows AttributeError

AttributeError: module 'src.exceptions' has no attribute 'an_exception_handler'

GREEN: make it pass


  • I add the name to exceptions.py

    5def raise_exception():
    6    raise Exception('BOOM!!!')
    7
    8
    9an_exception_handler
    

    the terminal shows NameError

    NameError: name 'an_exception_handler' is not defined
    
  • I point it to None to define it

    9an_exception_handler = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    
  • I make it a function

     9def an_exception_handler():
    10    return None
    

    the terminal shows TypeError

    TypeError: an_exception_handler() takes 0 positional arguments but 1 was given
    
  • I make the function take input

     9def an_exception_handler(the_input):
    10    return None
    

    the terminal shows AssertionError

    AssertionError: None != 'failed'
    

    the result of the call to src.exceptions.an_exception_handler is None and the test expects 'failed'

  • I change the return statement to match the expectation

     9def an_exception_handler(the_input):
    10    return 'failed'
    

    the test passes.


test_catching_success

I want an_exception_handler to process its input and return failed when an Exception happens or return success when an Exception is NOT raised.


RED: make it fail


I add a new test to test_exceptions.py

48      def test_catching_failure(self):
49          self.assertEqual(
50              src.exceptions.an_exception_handler(
51                  src.exceptions.raise_exception
52              ),
53              'failed'
54          )
55
56      def test_catching_success(self):
57          self.assertEqual(
58              src.exceptions.an_exception_handler(
59                  src.exceptions.does_not_raise_exception
60              ),
61              'succeeded'
62          )

the terminal shows AttributeError

AttributeError: module 'src.exceptions' has no attribute 'does_not_raise_exception'

GREEN: make it pass


  • I add the name to exceptions.py

     5def raise_exception():
     6    raise Exception('BOOM!!!')
     7
     8
     9does_not_raise_exception
    10
    11
    12def an_exception_handler(the_input):
    13    return 'failed'
    

    the terminal shows NameError

    NameError: name 'does_not_raise_exception' is not defined
    
  • I point it to None to define it

    9does_not_raise_exception = None
    

    the terminal shows AssertionError

    AssertionError: 'failed' != 'succeeded'
    

    src.exceptions.an_exception_handler still returns 'failed', the test expects 'succeeded'

  • I make an_exception_handler, remember the identity function

     9does_not_raise_exception = None
    10
    11
    12def an_exception_handler(the_input):
    13    return the_input
    14    return 'failed'
    

    the terminal shows AssertionError

    ...::test_catching_failure - AssertionError: <function raise_exception at 0xabcd12e34567> != 'failed'
    ...::TestExceptions::test_catching_success - AssertionError: None != 'succeeded'
    
    • test_catching_failure fails because an_exception_handler returns the name (raise_exception) and address in the computer(0xabcd12e34567) of the function it gets

    • test_catching_success fails because an_exception_handler returns does_not_raise_exception which points to None

  • I change the name of the input parameter to be clearer

    12def an_exception_handler(a_function):
    13    return a_function
    14    return 'failed'
    
  • I make an_exception_handler return the result of a call to its input as a function

    12def an_exception_handler(a_function):
    13    return a_function()
    14    return 'failed'
    

    the terminal shows TypeError

    a_function = None
    
        def an_exception_handler(a_function):
    >       return a_function()
    E       TypeError: 'NoneType' object is not callable
    

    because does_not_raise_exception points to None, which is not callable

  • I make it a function to make it callable

     9def does_not_raise_exception():
    10    return None
    11
    12
    13def an_exception_handler(a_function):
    14    return a_function()
    15    return 'failed'
    

    the terminal shows AssertionError

    AssertionError: None != 'succeeded'
    

    the result of calling src.exceptions.raise_exception in test_catching_failure is an Exception with a message

    Exception: 'BOOM!!!'
    

I need a way for the function to choose what to do when an Exception is raised and when one is NOT raised. I can use the try statement to do this

how to use try…except…else

  • I add a try statement to an_exception_handler in exceptions.py

    13def an_exception_handler(a_function):
    14    try:
    15        a_function()
    16    except Exception:
    17        return 'failed'
    

    test_catching_failure passes. The terminal still shows AssertionError for test_catching_success

    AssertionError: None != 'succeeded'
    

    the try statement is used to handle Exceptions in programs

  • I add an else clause for when a_function() runs without raising an Exception

    13def an_exception_handler(a_function):
    14    try:
    15        a_function()
    16    except Exception:
    17        return 'failed'
    18    else:
    19        return None
    

    the terminal shows AssertionError

    AssertionError: None != 'succeeded'
    

    this is still the same Exception and message

  • I change the return statement in the else clause

    13def an_exception_handler(a_function):
    14    try:
    15        a_function()
    16    except Exception:
    17        return 'failed'
    18    else:
    19        return 'succeeded'
    

    the test passes.

    The try statement is used to catch or handle Exceptions in Python. It allows the program to choose what it does when it runs into an Exception. I think of it as

    • try running this

    • except Exception - when running this raises Exception, run the code in this block

    • else - when running this does NOT raise Exception, run the code in this block

    In this case

    • try calling a_function()

    • except Exception - when calling a_function() raises Exception return 'failed'

    • else - when calling a_function() does NOT raise Exception return 'succeeded'

    the try statement is how I think of Test Driven Development or the scientific method

    • Try something

    • if it fails, try something else

    • do this as many times as you can until you get what you want

    or in the words of a famous singer …

  • I can be more specific with the Exception in the except block, for example

    13def an_exception_handler(a_function):
    14    try:
    15        a_function()
    16    except ModuleNotFoundError:
    17        return 'failed'
    18    else:
    19        return 'succeeded'
    

    the terminal shows Exception for test_catching_failures

    Exception: BOOM!!!!
    

    because Exception is not ModuleNotFoundError. The try statement only catches the Exception given in the except block and its children, all others are raised

  • I change it back to what works

    13def an_exception_handler(a_function):
    14    try:
    15        a_function()
    16    except Exception:
    17        return 'failed'
    18    else:
    19        return 'succeeded'
    

    the terminal shows green again! I know how to test that an Exception is raised and how to handle Exceptions (Errors) in programs. I am a master!!


close the project

  • I close exceptions.py and test_exceptions.py in the editor

  • I click in the terminal and exit the tests with ctrl+c on the keyboard, the terminal shows

    (.venv) .../pumping_python/exceptions
    
  • I deactivate the virtual environment

    deactivate
    

    the terminal goes back to the command line, (.venv) is no longer on the left side

    .../pumping_python/exceptions
    
  • I change directory to the parent of exceptions

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

I ran tests to show that


How many questions can you answer after going through this chapter?


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

you know

Would you like to handle ZeroDivisionError in the Calculator?


rate pumping python

If this has been a 7 star experience for you, please leave a 5 star review. It helps other people get into the book too