how to make a calculator part 3
test_calculator_raises_type_error
I want to use TypeError with exception handlers to make sure that the calculator program only works with numbers, the way a Calculator would in the real world.
open the project
I change directory to the
calculatorfoldercd calculatorthe terminal shows I am in the
calculatorfolder.../pumping_python/calculatorI activate the virtual environment
source .venv/bin/activateAttention
on Windows without Windows Subsystem for Linux use
.venv/bin/activate.ps1NOTsource .venv/bin/activate.venv/scripts/activate.ps1the terminal shows
(.venv) .../pumping_python/calculatorI use
pytest-watchto run the testspytest-watchthe terminal shows
rootdir: .../pumping_python/calculator collected 4 items tests/test_calculator.py .... [100%] ============================ 4 passed in X.YZs =============================I hold ctrl on the keyboard and click on
tests/test_calculator.pyto open it in the editor
RED: make it fail
I add a new failing test to show that the calculator raises TypeError when one of the inputs is None, just like in test_type_error_w_objects_that_do_not_mix
42 def test_division(self):
43 try:
44 self.assertEqual(
45 src.calculator.divide(
46 self.random_first_number,
47 self.random_second_number
48 ),
49 self.random_first_number/self.random_second_number
50 )
51 except ZeroDivisionError:
52 self.assertEqual(
53 src.calculator.divide(self.random_first_number, 0),
54 'undefined: I cannot divide by 0'
55 )
56
57 def test_calculator_raises_type_error_w_none(self):
58 src.calculator.add(None, None)
59
60
61# Exceptions seen
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
GREEN: make it pass
I add assertRaises
57 def test_calculator_raises_type_error_w_none(self):
58 with self.assertRaises(TypeError):
59 src.calculator.add(None, None)
the test passes
REFACTOR: make it better
I add a failing line for division
57 def test_calculator_raises_type_error_w_none(self): 58 with self.assertRaises(TypeError): 59 src.calculator.add(None None) 60 src.calculator.divide(None, None)TypeError: unsupported operand type(s) for /: 'NoneType' and 'NoneType'I add assertRaises
58 with self.assertRaises(TypeError): 59 src.calculator.add(self.random_first_number, None) 60 with self.assertRaises(TypeError): 61 src.calculator.divide(self.random_first_number, None)the test passes
I add another failing line, this time for multiplication
60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 src.calculator.multiply(None, None)TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'I add assertRaises
60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 with self.assertRaises(TypeError): 63 src.calculator.multiply(None, None)the test passes
I add another one for subtraction
62 with self.assertRaises(TypeError): 63 src.calculator.multiply(None, None) 64 src.calculator.subtract(None, None)TypeError: unsupported operand type(s) for -: 'NoneType' and 'NoneType'I add the assertRaises method
57 def test_calculator_raises_type_error_w_none(self): 58 with self.assertRaises(TypeError): 59 src.calculator.add(None, None) 60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 with self.assertRaises(TypeError): 63 src.calculator.multiply(None, None) 64 with self.assertRaises(TypeError): 65 src.calculator.subtract(None, None) 66 67 68# Exceptions seenthe test passes
The calculator raises TypeError when given None as input. What does it do when the input is a boolean, string, tuple, list, set or a dictionary?
test_calculator_raises_type_error_w_strings
RED: make it fail
I add a new test with an assertion from test_what_is_an_assertion to test the add function with strings
64 with self.assertRaises(TypeError):
65 src.calculator.subtract(None, None)
66
67 def test_calculator_with_strings(self):
68 self.assertEqual(src.calculator.add('1', '1'), '2')
69
70
71# Exceptions seen
the terminal shows AssertionError
AssertionError: '11' != '2'
GREEN: make it pass
I change the expectation to match reality
68 self.assertEqual(src.calculator.add('1', '1'), '11')
the test passes
REFACTOR: make it better
I add an assertion for the divide function
67 def test_calculator_with_strings(self): 68 self.assertEqual(src.calculator.add('1', '1'), '11') 69 self.assertEqual(src.calculator.divide('1', '1'), '11')TypeError: unsupported operand type(s) for /: 'str' and 'str'I change assertEqual to assertRaises
68 def test_calculator_with_strings(self): 69 self.assertEqual(src.calculator.add('1', '1'), '11') 70 with self.assertRaises(TypeError): 71 src.calculator.divide('1', '1')the test passes
I try it with the multiply function
69 with self.assertRaises(TypeError): 70 src.calculator.divide('1', '1') 71 src.calculator.multiply('1', '1')TypeError: can't multiply sequence by non-int of type 'str'I add assertRaises
69 with self.assertRaises(TypeError): 70 src.calculator.divide('1', '1') 71 with self.assertRaises(TypeError): 72 src.calculator.multiply('1', '1')the test passes
I add an assertion for the subtract function
71 with self.assertRaises(TypeError): 72 src.calculator.multiply('1', '1') 73 src.calculator.subtract('1', '1')TypeError: unsupported operand type(s) for -: 'str' and 'str'I add assertRaises
57 def test_calculator_w_strings(self): 58 with self.assertRaises(TypeError): 59 src.calculator.add(None, None) 60 with self.assertRaises(TypeError): 61 src.calculator.divide(None, None) 62 with self.assertRaises(TypeError): 63 src.calculator.multiply('1', '1') 64 with self.assertRaises(TypeError): 65 src.calculator.subtract('1', '1')the test passes
how to test if something is an instance of an object in a program
I want the add function to raise TypeError when it gets a string, the same way the other functions raise TypeError when one of the inputs is a string. I can use the isinstance function which is like the assertIsInstance method from when I tested None, it checks if one thing is an instance or child of a class
I change the assertEqual to assertRaises in
test_calculator_with_stringsintest_calculator.py67 def test_calculator_with_strings(self): 68 with self.assertRaises(TypeError): 69 src.calculator.add('1', '1') 70 with self.assertRaises(TypeError): 71 src.calculator.divide('1', '1')the terminal shows AssertionError
AssertionError: TypeError not raisedthen I add an if statement to the add function in
calculator.py16def add(first_input, second_input): 17 if isinstance(first_input, str) or isinstance(second_input, str): 18 raise TypeError 19 else: 20 return first_input + second_inputthe test passes
Note
the isinstance function like the assertIsInstance method checks if the first input it is given is an instance (child) of the class it is given as the second input. It is part of Python’s Built-in Functions
the if statement
if isinstance(first_input, str) or isinstance(second_input, str):is True ifthe statement is only False if
first_inputis NOT a string andsecond_inputis NOT a string. This is Logical Disjunction from the Truth Table, which only returns False when the two inputs are False
I change the name of the test to be clearer
67 def test_calculator_raises_type_error_w_strings(self): 68 with self.assertRaises(TypeError): 69 src.calculator.add('1', '1') 70 with self.assertRaises(TypeError): 71 src.calculator.divide('1', '1') 72 with self.assertRaises(TypeError): 73 src.calculator.multiply('1', '1') 74 with self.assertRaises(TypeError): 75 src.calculator.subtract('1', '1') 76 77 78# Exceptions seen
test_calculator_sends_message_when_input_is_not_a_number
I want the calculator functions to send a message when the input is not a number, not raise TypeError which causes the program to stop. I want the user to be able to try again with different input
RED: make it fail
I change the assertRaises to assertEqual for the add function in test_calculator_raises_type_error_w_none
57 def test_calculator_raises_type_error_w_none(self):
58 self.assertEqual(
59 src.calculator.add(None, None),
60 'Excuse me?! Numbers only! try again...'
61 )
62 with self.assertRaises(TypeError):
63 src.calculator.divide(None, None)
64 with self.assertRaises(TypeError):
65 src.calculator.multiply(None, None)
66 with self.assertRaises(TypeError):
67 src.calculator.subtract(None, None)
TypeError: unsupported operand type(s) for +: 'NoneType' and 'NoneType'
GREEN: make it pass
I add an exception handler to the else clause of the add function in calculator.py
16def add(first_input, second_input):
17 if isinstance(first_input, str) or isinstance(second_input, str):
18 raise TypeError
19 else:
20 try:
21 return first_input + second_input
22 except TypeError:
23 return 'Excuse me?! Numbers only! try again...'
the test passes
REFACTOR: make it better
I want the same thing to happen when the add function gets a string as input. I change the assertRaises to assertEqual for the add function in
test_calculator_raises_type_error_w_stringsintest_calculator.py70 def test_calculator_raises_type_error_w_strings(self): 71 self.assertEqual( 72 src.calculator.add('1', '1'), 73 'Excuse me?! Numbers only! try again...' 74 ) 75 with self.assertRaises(TypeError): 76 src.calculator.divide('1', '1')TypeErrorI change the raise statement to a return statement in the add function in
calculator.py16def add(first_input, second_input): 17 if isinstance(first_input, str) or isinstance(second_input, str): 18 return 'Excuse me?! Numbers only! try again...' 19 else: 20 try: 21 return first_input + second_input 22 except TypeError: 23 return 'Excuse me?! Numbers only! try again...'the test passes
I change the assertRaises to assertEqual for the divide function in
test_calculator_raises_type_error_w_stringsintest_calculator.py69 def test_calculator_raises_type_error_w_strings(self): 70 self.assertEqual( 71 src.calculator.add('1', '1'), 72 'Excuse me?! Numbers only! try again...' 73 ) 74 self.assertEqual( 75 src.calculator.divide('1', '1'), 76 'Excuse me?! Numbers only! try again...' 77 )TypeError: unsupported operand type(s) for /: 'str' and 'str'I add another except clause to the exception handler in the divide function in
calculator.py9def divide(first_input, second_input): 10 try: 11 return first_input / second_input 12 except ZeroDivisionError: 13 return 'undefined: I cannot divide by 0' 14 except TypeError: 15 return 'Excuse me?! Numbers only! try again...'the terminal shows AssertionError
AssertionError: TypeError not raisedtest_calculator_raises_type_error_w_nonefails because it expects TypeError when the inputs are not numbersI change the assertRaises to assertEqual for the divide function in
test_calculator_raises_type_error_w_noneintest_calculator.py57def test_calculator_raises_type_error_w_none(self): 58 self.assertEqual( 59 src.calculator.add(None, None), 60 'Excuse me?! Numbers only! try again...' 61 ) 62 self.assertEqual( 63 src.calculator.divide(None, None), 64 'Excuse me?! Numbers only! try again...' 65 )the test passes
I change the assertRaises to assertEqual for the multiply function in
test_calculator_raises_type_error_w_none62 self.assertEqual( 63 src.calculator.divide(None, None), 64 'Excuse me?! Numbers only! try again...' 65 ) 66 self.assertEqual( 67 src.calculator.multiply(None, None), 68 'Excuse me?! Numbers only! try again...' 69 )TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'I add an exception handler to the multiply function in
calculator.py5def multiply(first_input, second_input): 6 try: 7 return first_input * second_input 8 except TypeError: 9 return 'Excuse me?! Numbers only! try again...'the terminal shows AssertionError
AssertionError: TypeError not raisedI change the assertRaises to assertEqual for the multiply function in
test_calculator_raises_type_error_w_stringsintest_calculator.py78 self.assertEqual( 79 src.calculator.divide('1', '1'), 80 'Excuse me?! Numbers only! try again...' 81 ) 82 self.assertEqual( 83 src.calculator.multiply('1', '1'), 84 'Excuse me?! Numbers only! try again...' 85 )the test passes
I change the assertRaises to assertEqual for the subtract function in
test_calculator_raises_type_error_w_strings84 self.assertEqual( 85 src.calculator.multiply('1', '1'), 86 'Excuse me?! Numbers only! try again...' 87 ) 88 self.assertEqual( 89 src.calculator.subtract('1', '1'), 90 'Excuse me?! Numbers only! try again...' 91 ) 92 93 94# Exceptions seenTypeError: unsupported operand type(s) for -: 'str' and 'str'I add an exception handler to the subtract function in
calculator.py1def subtract(first_input, second_input): 2 try: 3 return first_input - second_input 4 except TypeError: 5 return 'Excuse me?! Numbers only! try again...'the terminal shows AssertionError
AssertionError: TypeError not raisedI change the assertRaises to assertEqual for the subtract function in
test_calculator_raises_type_error_w_noneintest_calculator.py66 self.assertEqual( 67 src.calculator.multiply(None, None), 68 'Excuse me?! Numbers only! try again...' 69 ) 70 self.assertEqual( 71 src.calculator.subtract(None, None), 72 'Excuse me?! Numbers only! try again...' 73 ) 74 75 76 def test_calculator_raises_type_error_w_strings(self):the test passes
That was a lot of doing the same thing over and over again.
test_calculator_raises_type_error_w_noneandtest_calculator_raises_type_error_w_stringsboth look the samethe calculator no longer raises TypeError when any of the inputs are NOT a number
I remove the name of
test_calculator_raises_type_error_w_stringsto make its assertions part oftest_calculator_raises_type_error_w_none70 self.assertEqual( 71 src.calculator.subtract(None, None), 72 'Excuse me?! Numbers only! try again...' 73 ) 74 self.assertEqual( 75 src.calculator.add('1', '1'), 76 'Excuse me?! Numbers only! try again...' 77 ) 78 self.assertEqual( 79 src.calculator.divide('1', '1'), 80 'Excuse me?! Numbers only! try again...' 81 ) 82 self.assertEqual( 83 src.calculator.multiply('1', '1'), 84 'Excuse me?! Numbers only! try again...' 85 ) 86 self.assertEqual( 87 src.calculator.subtract('1', '1'), 88 'Excuse me?! Numbers only! try again...' 89 ) 90 91 92# Exceptions seenI change the name from
test_calculator_raises_type_error_w_nonetotest_calculator_sends_message_when_input_is_not_a_numberto be clearer57 def test_calculator_sends_message_when_input_is_not_a_number(self): 58 self.assertEqual(the tests are still green
I have the same error message 8 times in this test. I use a variable to make it better
57 def test_calculator_sends_message_when_input_is_not_a_number(self): 58 error_message = 'Excuse me?! Numbers only! try again...' 59 60 self.assertEqual( 61 src.calculator.add(None, None), 62 error_message 63 ) 64 self.assertEqual( 65 src.calculator.divide(None, None), 66 error_message 67 ) 68 self.assertEqual( 69 src.calculator.multiply(None, None), 70 error_message 71 ) 72 self.assertEqual( 73 src.calculator.subtract(None, None), 74 error_message 75 ) 76 self.assertEqual( 77 src.calculator.add('1', '1'), 78 error_message 79 ) 80 self.assertEqual( 81 src.calculator.divide('1', '1'), 82 error_message 83 ) 84 self.assertEqual( 85 src.calculator.multiply('1', '1'), 86 error_message 87 ) 88 self.assertEqual( 89 src.calculator.subtract('1', '1'), 90 error_message 91 )still green. All these assertions look the same, they check that the calculator functions return an error message when they get input that is NOT a number
self.assertEqual( src.calculator.function(NOT_A_NUMBER, ALSO_NOT_A_NUMBER), error_message )there has to be a better way to test the calculator with inputs that are NOT numbers
how to make a decorator function
All the functions in the calculator program have the same exception handler
try:
something
except TypeError:
return 'Excuse me?! Numbers only! try again...'
the divide function is different because it has another except clause
except ZeroDivisionError:
return 'undefined: I cannot divide by 0'
the other part that is different for all the functions are the calculations
return first_input - second_input
return first_input * second_input
return first_input / second_input
return first_input + second_input
what is a decorator function?
A decorator or wrapper function takes another function as input and returns a function. I can use it to remove the exception handler that is the same in all of the calculator functions
I add a new function add the top of
calculator.py1def only_takes_numbers(function): 2 def wrapper(first_input, second_input): 3 try: 4 return function(first_input, second_input) 5 except TypeError: 6 return 'Excuse me?! Numbers only! try again...' 7 return wrapper 8 9 10def subtract(first_input, second_input):I use it to wrap the subtract function
10@only_takes_numbers 11def subtract(first_input, second_input): 12 try:the test is still green
I remove the parts that are also in the
only_takes_numbersfunction15@only_takes_numbers 16def subtract(first_input, second_input): 17 return first_input - second_input 18 19 20def multiply(first_input, second_input):still green
I do the same thing with the multiply function
15@only_takes_numbers 16def multiply(first_input, second_input): 17 try:the terminal shows green
I remove the parts that are also in the
only_takes_numbersfunction15@only_takes_numbers 16def multiply(first_input, second_input): 17 return first_input * second_input 18 19 20def divide(first_input, second_input):the tests are still passing
on to the divide function
20@only_takes_numbers 21def divide(first_input, second_input): 22 try:still green
I remove the except clause for TypeError
20@only_takes_numbers 21def divide(first_input, second_input): 22 try: 23 return first_input / second_input 24 except ZeroDivisionError: 25 return 'undefined: I cannot divide by 0' 26 27 28def add(first_number, second_input):all the tests are still green
one more to go, I wrap the add function with the
only_takes_numbersfunction28@only_takes_numbers 29def add(first_input, second_input): 30 if isinstance(first_input, str) or isinstance(second_input, str):the test is still passing
I remove the exception handler from the else clause
28@only_takes_numbers 29def add(first_input, second_input): 30 if isinstance(first_input, str) or isinstance(second_input, str): 31 return 'Excuse me?! Numbers only! try again...' 32 else: 33 return first_input + second_inputgreen! Lovely!
I can make a function for the condition in the if statement in the add function in
calculator.py28def is_string(something): 29 return isinstance(something, str) 30 31 32@only_takes_numbers 33def add(first_input, second_input): 34 if is_string(first_input) or is_string(second_input): 35 return 'Excuse me?! Numbers only! try again...' 36 else: 37 return first_input + second_inputthe test is still green
This removes the duplication of
strin the call to the isinstance functionisinstance(first_input, str) isinstance(second_input, str):it also adds 2 lines of code to remove 6 characters. WOW!
I can make a function for the whole if statement in the add function
28def one_input_is_a_string(first_input, second_input): 29 return isinstance(first_input, str) or isinstance(second_input, str) 30 31 32@only_takes_numbers 33def add(first_input, second_input): 34 if one_input_is_a_string(first_input, second_input): 35 return 'Excuse me?! Numbers only! try again...' 36 else: 37 return first_input + second_inputthe test is still green.
the
one_input_is_a_stringfunction looks the same as Logical Disjunction from the Truth Tablethis makes it easier to change the condition later without touching the add function
it still adds 2 lines of code
I can also make a decorator function for the if statement to practice making a decorator function
1def reject_strings(function): 2 def wrapper(first_input, second_input): 3 if isinstance(first_input, str) or isinstance(second_input, str): 4 return 'Excuse me?! Numbers only! try again...' 5 else: 6 return function(first_input, second_input) 7 return wrapper 8 9 10def only_takes_numbers(function):then use it to wrap the add function
37@reject_strings 38@only_takes_numbers 39def add(first_input, second_input):the test is still green
I remove the if statement from the add function
37@reject_strings 38@only_takes_numbers 39def add(first_input, second_input): 40 return first_input + second_inputthe test is still green
the
reject_stringsandonly_takes_numbersfunctions have parts that are the samedef wrapper(first_input, second_input): ... return 'Excuse me?! Numbers only! try again...' ... return function(first_input, second_input) return wrapperI make a new function that has the if statement from
reject_stringsand the exception handler fromonly_takes_numbers1def only_takes_numbers_and_rejects_strings(function): 2 def wrapper(first_input, second_input): 3 if isinstance(first_input, str) or isinstance(second_input, str): 4 return 'Excuse me?! Numbers only! try again...' 5 else: 6 try: 7 return function(first_input, second_input) 8 except TypeError: 9 return 'Excuse me?! Numbers only! try again...' 10 return wrapper 11 12 13def reject_strings(function):I use the new function to wrap the add function
49@only_takes_numbers_and_rejects_strings 50@reject_strings 51@only_takes_numbers 52def add(first_input, second_input): 53 return first_input + second_inputthe test is still green
I remove the other wrappers from the add function
49@only_takes_numbers_and_rejects_strings 50def add(first_input, second_input): 51 return first_input + second_inputstill green
I wrap the other functions with
only_takes_numbers_and_reject_strings31@only_takes_numbers_and_rejects_strings 32@only_takes_numbers 33def subtract(first_input, second_input): 34 return first_input - second_input 35 36 37@only_takes_numbers_and_rejects_strings 38@only_takes_numbers 39def multiply(first_input, second_input): 40 return first_input * second_input 41 42 43@only_takes_numbers_and_rejects_strings 44@only_takes_numbers 45def divide(first_input, second_input): 46 try: 47 return first_input / second_input 48 except ZeroDivisionError: 49 return 'undefined: I cannot divide by 0' 50 51 52@only_takes_numbers_and_rejects_strings 53def add(first_input, second_input):the terminal shows green
I remove
only_takes_numbersfrom each function31@only_takes_numbers_and_rejects_strings 32def subtract(first_input, second_input): 33 return first_input - second_input 34 35 36@only_takes_numbers_and_rejects_strings 37def multiply(first_input, second_input): 38 return first_input * second_input 39 40 41@only_takes_numbers_and_rejects_strings 42def divide(first_input, second_input): 43 try: 44 return first_input / second_input 45 except ZeroDivisionError: 46 return 'undefined: I cannot divide by 0' 47 48 49@only_takes_numbers_and_rejects_strings 50def add(first_input, second_input): 51 return first_input + second_inputthe test is still green
I remove the
reject_stringsandonly_takes_numbersfunctions1def only_takes_numbers_and_rejects_strings(function): 2 def wrapper(first_input, second_input): 3 if isinstance(first_input, str) or isinstance(second_input, str): 4 return 'Excuse me?! Numbers only! try again...' 5 else: 6 try: 7 return function(first_input, second_input) 8 except TypeError: 9 return 'Excuse me?! Numbers only! try again...' 10 return wrapper 11 12 13@only_takes_numbers_and_rejects_strings 14def subtract(first_input, second_input): 15 return first_input - second_input 16 17 18@only_takes_numbers_and_rejects_strings 19def multiply(first_input, second_input): 20 return first_input * second_input 21 22 23@only_takes_numbers_and_rejects_strings 24def divide(first_input, second_input): 25 try: 26 return first_input / second_input 27 except ZeroDivisionError: 28 return 'undefined: I cannot divide by 0' 29 30 31@only_takes_numbers_and_rejects_strings 32def add(first_input, second_input): 33 return first_input + second_inputI change the name of the new decorator function to make it easier
1def only_takes_numbers(function): 2 def wrapper(first_input, second_input): 3 if isinstance(first_input, str) or isinstance(second_input, str): 4 return 'Excuse me?! Numbers only! try again...' 5 else: 6 try: 7 return function(first_input, second_input) 8 except TypeError: 9 return 'Excuse me?! Numbers only! try again...' 10 return wrapper 11 12 13@only_takes_numbers 14def subtract(first_input, second_input): 15 return first_input - second_input 16 17 18@only_takes_numbers 19def multiply(first_input, second_input): 20 return first_input * second_input 21 22 23@only_takes_numbers 24def divide(first_input, second_input): 25 try: 26 return first_input / second_input 27 except ZeroDivisionError: 28 return 'undefined: I cannot divide by 0' 29 30 31@only_takes_numbers 32def add(first_input, second_input): 33 return first_input + second_inputstill green
There is also duplication of the error message. I add a variable to remove it
1def only_takes_numbers(function): 2 def wrapper(first_input, second_input): 3 error_message = 'Excuse me?! Numbers only! try again...' 4 5 if isinstance(first_input, str) or isinstance(second_input, str): 6 return error_message 7 else: 8 try: 9 return function(first_input, second_input) 10 except TypeError: 11 return error_message 12 return wrapperand all the tests are still passing. The world is my oyster!
close the project
I close
test_calculator.pyandcalculator.pyin the editorI click in the terminal and exit the tests with ctrl+c on the keyboard, the terminal shows
(.venv) .../pumping_python/calculatorI deactivate the virtual environment
deactivatethe terminal goes back to the command line,
(.venv)is no longer on the left side.../pumping_python/calculatorI change directory to the parent of
calculatorcd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectory
code from the chapter
what is next?
you know
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