how to handle Exceptions in programs
preview
I have these tests by the end of the chapter
1import src.exceptions
2import unittest
3
4
5class TestExceptions(unittest.TestCase):
6
7 @staticmethod
8 def assert_raises(code, exception):
9 try:
10 exec(code)
11 except exception:
12 pass
13 else:
14 raise AssertionError(f'{exception} not raised')
82 def test_catching_exceptions_w_messages(self):
83 with self.assertRaisesRegex(
84 Exception, 'BOOM!!!'
85 ):
86 src.exceptions.raise_exception()
87
88 def test_catching_failure(self):
89 self.assertEqual(
90 src.exceptions.an_exception_handler(
91 src.exceptions.raise_exception
92 ),
93 'failed'
94 )
96 def test_catching_success(self):
97 self.assertEqual(
98 src.exceptions.an_exception_handler(
99 src.exceptions.function_name
100 ),
101 'succeeded'
102 )
103
104
105# Exceptions seen
106# AssertionError
107# ModuleNotFoundError
108# NameError
109# AttributeError
110# TypeError
111# SyntaxError
112# IndexError
113# KeyError
114# ZeroDivisionError
requirements
open the project
I change directory to the
exceptionsfoldercd exceptionsI use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe terminal is my friend, and shows
rootdir: .../pumping_python/exceptions configfile: pyproject.toml collected 8 items tests/test_exceptions.py ........ [100%] ==================== 8 passed in X.YZs ======================I hold ctrl on the keyboard, then click on
tests/test_exceptions.pyto open it
test_catching_exceptions_w_messages
RED: make it fail
I add a failing test to
tests/test_exceptions.py77 def test_catching_exceptions(self): 78 self.assert_raises('raise Exception', Exception) 79 with self.assertRaises(Exception): 80 raise Exception 81 82 def test_catching_exceptions_w_messages(self): 83 src.exceptions.raise_exception() 84 85 86# Exceptions seenthe terminal is my friend, and shows AttributeError
AttributeError: module 'src.exceptions' has no attribute 'raise_exception'
GREEN: make it pass
I add the name to
src/exceptions/__init__.py1def function_name(): 2 return None 3 4 5raise_exceptionthe terminal is my friend, and shows NameError
NameError: name 'raise_exception' is not definedI point it to None to define it
5raise_exception = Nonethe terminal is my friend, and shows TypeError
TypeError: 'NoneType' object is not callableI make
raise_exceptiona function to make it callable5def raise_exception(): 6 return Nonethe test passes.
REFACTOR: make it better
I want the function to raise Exception when it is called as a way to make a failure happen. I add assertRaises to test_catching_exceptions_w_messages in
tests/test_exceptions.py82 def test_catching_exceptions_w_messages(self): 83 with self.assertRaises(Exception): 84 src.exceptions.raise_exception()the terminal is my friend, and shows AssertionError
AssertionError: Exception not raisedI add a raise statement to the
raise_exceptionfunction insrc/exceptions/__init__.py5def raise_exception(): 6 raise Exceptionthe test passes.
how to test the message of an Exception
I can use the assertRaisesRegex method to test what message I get with an Exception. It helps tell the difference between two Exceptions with the same name.
RED: make it fail
I change assertRaises to assertRaisesRegex in test_catching_exceptions_w_messages in tests/test_exceptions.py
82 def test_catching_exceptions_w_messages(self):
83 with self.assertRaisesRegex(
84 Exception, 'BOOM!!!'
85 ):
86 src.exceptions.raise_exception()
the terminal is my friend, and shows AssertionError
AssertionError: "BOOM!!!" does not match ""
because the Exception raised by the raise_exception function has no message and the assertRaisesRegex method checks that the code in its context (src.exceptions.raise_exception()) raises the Exception it is given, with the message it is given ('BOOM!!!').
The default message of the Exception is the empty string ('') and the assertion expects "BOOM!!!"
GREEN: make it pass
I add the expected message in src/exceptions/__init__.py
5def raise_exception():
6 raise Exception('BOOM!!!')
the test passes.
REFACTOR: make it better
This means I can make my assert_raises method have a message like unittest.TestCase.assertRaises.
I add an f-string to my assert_raises method in
tests/test_exceptions.py7 @staticmethod 8 def assert_raises(code, exception): 9 try: 10 exec(code) 11 except exception: 12 pass 13 else: 14 raise AssertionError(f'{exception} not raised') 15 16 def test_catching_module_not_found_error(self):I change the statement in test_catching_module_not_found_error to make the test fail
16 def test_catching_module_not_found_error(self): 17 self.assert_raises( 18 # 'import does_not_exist', ModuleNotFoundError 19 'import src.exceptions', ModuleNotFoundError 20 ) 21 with self.assertRaises(ModuleNotFoundError): 22 import does_not_exist 23 24 def test_catching_name_error(self):the terminal is my friend, and shows AssertionError
AssertionError: <class 'ModuleNotFoundError'> not raisedit is now closer to the message of the assertRaises method, just not as good.
I undo the change to test_catching_module_not_found_error
16def test_catching_module_not_found_error(self): 17 self.assert_raises( 18 'import does_not_exist', ModuleNotFoundError 19 ) 20 with self.assertRaises(ModuleNotFoundError): 21 import does_not_exist 22 23def test_catching_name_error(self):the test is green again.
I add a git commit message
git commit -am 'add test_catching_exceptions_w_messages'
In some cases I want to send a message to the user instead of the Exception which they may not understand.
I might also want the program to make a decision if an Exception happens so it continues without stopping.
I want the program to process its input and return failed if an Exception is raised while processing the input or return success if an Exception is NOT raised.
Exception |
output |
|---|---|
raised |
failed |
NOT raised |
success |
test_catching_failure
RED: make it fail
I add a new test for if a function is called and an Exception is raised to tests/test_exceptions.py
Exception |
output |
|---|---|
raised |
failed |
82 def test_catching_exceptions_w_messages(self):
83 with self.assertRaisesRegex(
84 Exception, 'BOOM!!!'
85 ):
86 src.exceptions.raise_exception()
87
88 def test_catching_failure(self):
89 self.assertEqual(
90 src.exceptions.an_exception_handler(
91 src.exceptions.raise_exception
92 ),
93 'failed'
94 )
the terminal is my friend, and shows AttributeError
AttributeError: module 'src.exceptions'
has no attribute 'an_exception_handler'
GREEN: make it pass
I add
an_exception_handlertosrc/exceptions/__init__.py5def raise_exception(): 6 raise Exception('BOOM!!!') 7 8 9an_exception_handlerthe terminal is my friend, and shows NameError
NameError: name 'an_exception_handler' is not definedI point
an_exception_handlerto None to define it9an_exception_handler = Nonethe terminal is my friend, and shows TypeError
TypeError: 'NoneType' object is not callableI make
an_exception_handlera function9def an_exception_handler(): 10 return Nonethe terminal is my friend, and shows TypeError
TypeError: an_exception_handler() takes 0 positional arguments but 1 was givenI make
an_exception_handlertake input9def an_exception_handler(the_input): 10 return Nonethe terminal is my friend, and shows AssertionError
AssertionError: None != 'failed'the result of the call to
src.exceptions.an_exception_handleris None and the assertion expects'failed'.I change the return statement to match the expectation
9def an_exception_handler(the_input): 10 return 'failed'the test passes.
I add a git commit message
test_catching_success
RED: make it fail
I add a test for if an_exception_handler is called and an Exception is NOT raised, in tests/test_exceptions.py
Exception |
output |
|---|---|
NOT raised |
success |
88 def test_catching_failure(self):
89 self.assertEqual(
90 src.exceptions.an_exception_handler(
91 src.exceptions.raise_exception
92 ),
93 'failed'
94 )
95
96 def test_catching_success(self):
97 self.assertEqual(
98 src.exceptions.an_exception_handler(
99 src.exceptions.function_name
100 ),
101 'succeeded'
102 )
103
104
105# Exceptions seen
the terminal is my friend, and shows AssertionError
AssertionError: 'failed' != 'succeeded'
src.exceptions.an_exception_handler always returns 'failed', the assertion expects 'succeeded'
GREEN: make it pass
I make
an_exception_handlerreturn its input9def an_exception_handler(the_input): 10 return the_input 11 return 'failed'the terminal is my friend, and shows AssertionError
...test_catching_failure - AssertionError: <function raise_exception at 0xabcd12e34567> != 'failed' ...test_catching_success - AssertionError: <function function_name at 0xfecdb8a7f6e5> != 'succeeded'both tests fail because
an_exception_handlerreturns the name and address in the computer of the function it receives.I change the name of the input parameter to make it clearer
9def an_exception_handler(a_function): 10 return a_function 11 return 'failed'I make
an_exception_handlerreturn the result of a call to its input9def an_exception_handler(a_function): 10 return a_function() 11 return 'failed'the terminal is my friend, and shows Exception and AssertionError
FAILED ...test_catching_failure - Exception: BOOM!!! FAILED ...test_catching_success - AssertionError: None != 'succeeded'because if
an_exception_handleris called, it calls the input it receivesIf the call to its input raises an Exception, the program stops
src.exceptions.an_exception_handler( src.exceptions.raise_exception ) └── def an_exception_handler(a_function): └── return a_function() └── def raise_exception(): └── raise Exception('BOOM!!!')If the call to its input does NOT raise an Exception, it returns the result of the call to its input
src.exceptions.an_exception_handler( src.exceptions.function_name ) └── def an_exception_handler(a_function): └── return a_function() └── def function_name(): └── return None
I add a try statement to
an_exception_handlerto make it choose what to do if an Exception is raised, insrc/exceptions/__init__.py9def an_exception_handler(a_function): 10 try: 11 return a_function() 12 except Exception: 13 return 'failed'test_catching_failure passes. The terminal still shows AssertionError for test_catching_success
AssertionError: None != 'succeeded'because
an_exception_handlerreturns the result of calling thefunction_namefunction which is None.I add else to the try statement for if
a_function()runs and does NOT raise an Exception, to make it clearer9def an_exception_handler(a_function): 10 try: 11 a_function() 12 except Exception: 13 return 'failed' 14 else: 15 return Nonethe terminal still shows AssertionError.
I change the return statement in the else clause to give the test what it wants
9def an_exception_handler(a_function): 10 try: 11 a_function() 12 except Exception: 13 return 'failed' 14 else: 15 return 'succeeded'the test passes.
I can be more explicit with the Exception in the except block
9def an_exception_handler(a_function): 10 try: 11 a_function() 12 # except Exception: 13 except ModuleNotFoundError: 14 return 'failed' 15 else: 16 return 'succeeded'the terminal is my friend, and shows Exception for
test_catching_failureException: BOOM!!!because Exception is not ModuleNotFoundError and I cannot use a child Exceptions to catch its parent Exception.
The try statement only catches the Exception given in the except clause and its children (subclasses), all other Exceptions are raised.
I change it back to what works
9def an_exception_handler(a_function): 10 try: 11 a_function() 12 except Exception: 13 return 'failed' 14 else: 15 return 'succeeded'the test is green again!
I add a git commit message
The try statement is used to catch or handle Exceptions in Python. It allows the program to choose what to do if it runs into an Exception. I think of it as
trysomethingexcept Exception- if something raises Exception, run the code in this blockelse- something does NOT raise Exception, run the code in this block
In this case
trya_function()def an_exception_handler(a_function): └── try: └── a_function() ...except Exception- if a_function() raises Exception return'failed'src.exceptions.an_exception_handler( src.exceptions.raise_exception ) └── def an_exception_handler(a_function): └── try: └── a_function() └── def raise_exception(): ┌───────────┴── raise Exception('BOOM!!!') └── except Exception: └── return 'failed' else: return 'succeeded'else- if a_function() does NOT raise Exception return'succeeded'src.exceptions.an_exception_handler( src.exceptions.function_name ) └── def an_exception_handler(a_function): └── try: ┌───┴── a_function() │ └── def function_name(): │ └── return None │ except Exception: │ return 'failed' └── else: └── 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 …
close the project
I close
src/exceptions/__init__.pyandtests/test_exceptions.pyI click in the terminal where the tests are running
I use q on the keyboard to leave the tests. The terminal goes back to the command line.
I change directory to the parent of
exceptionscd ..
review
I ran tests to show that
I can use assertRaisesRegex to catch Exceptions with messages.
I can use try..except…else to make programs that can choose what to do when Exceptions are raised.
How many questions can you answer after going through this chapter?
code from the chapter
what is next?
rate pumping python
If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too.