how to make a calculator 4

I want to use TypeError with exception handlers to make sure that the calculator program only works with numbers, like a Calculator in the real world.


preview

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


open the project

  • I change directory to the calculator folder

    cd calculator
    

    the terminal shows I am in the calculator folder

    .../pumping_python/calculator
    
  • I use pytest-watcher to run the tests

    uv run pytest-watcher . --now
    

    the terminal shows

    rootdir: .../pumping_python/calculator
    configfile: pyproject.toml
    collected 4 items
    
    tests/test_calculator.py ....                                        [100%]
    
    ======================== 4 passed in X.YZs =========================
    
  • I hold ctrl on the keyboard, then click on tests/test_calculator.py to open it in the editor


test_calculator_raises_type_error_w_none


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                'brmph?! I cannot divide by 0. Try again...'
55            )
56
57    def test_calculator_raises_type_error_w_none(self):
58        src.calculator.add(None, None)
59
60
61# Exceptions seen

the terminal shows TypeError

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)
    

    the terminal shows TypeError

    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)
    

    the terminal shows TypeError

    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)
    

    the terminal shows TypeError

    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 seen
    

    the test passes

The calculator raises TypeError when None is given as input.

What does it do if 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')
    

    the terminal shows TypeError

    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')
    

    the terminal shows TypeError

    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')
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for -: 'str' and 'str'
    
  • I add assertRaises

    67    def test_calculator_with_strings(self):
    68        self.assertEqual(src.calculator.add('1', '1'), '11')
    69        with self.assertRaises(TypeError):
    70            src.calculator.divide('1', '1')
    71        with self.assertRaises(TypeError):
    72            src.calculator.multiply('1', '1')
    73        with self.assertRaises(TypeError):
    74            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_strings

    67    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 raised
    
  • I open calculator.py from the src folder in the editor

  • then I add an if statement to the add function in calculator.py

    16def add(first_input, second_input):
    17    if (
    18        isinstance(first_input, str)
    19        or
    20        isinstance(second_input, str)
    21    ):
    22        raise TypeError
    23    else:
    24        return first_input + second_input
    

    the test passes

    Note

    This is Logical Disjunction from the Truth Table, which only returns False, if the two inputs are False

  • I change the name of the test to say what it does

    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 it gets something that 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            'brmph?! 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)

the terminal shows TypeError

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 (
18        isinstance(first_input, str)
19        or
20        isinstance(second_input, str)
21    ):
22        raise TypeError
23    else:
24        try:
25            return first_input + second_input
26        except TypeError:
27            return 'brmph?! Numbers only. Try again...'

the test passes


REFACTOR: make it better


  • I change the assertRaises to assertEqual for the divide function

    57    def test_calculator_raises_type_error_w_none(self):
    58        self.assertEqual(
    59            src.calculator.add(None, None),
    60            'brmph?! Numbers only. Try again...'
    61        )
    62        self.assertEqual(
    63            src.calculator.divide(None, None),
    64            'brmph?! Numbers only. Try again...'
    65        )
    66        with self.assertRaises(TypeError):
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for /: 'NoneType' and 'NoneType'
    
  • I add another except clause to the exception handler in the divide function in calculator.py

     9def divide(first_input, second_input):
    10    try:
    11        return first_input / second_input
    12    except ZeroDivisionError:
    13        return 'brmph?! I cannot divide by 0. Try again...'
    14    except TypeError:
    15        return 'brmph?! Numbers only. Try again...'
    

    the terminal shows AssertionError

    AssertionError: TypeError not raised
    

    my change made the assertion in test_calculator_raises_type_error_w_strings fail

  • I undo the change, then add an if statement

     9def divide(first_input, second_input):
    10    if first_input is None or second_input is None:
    11        return 'brmph?! Numbers only. Try again...'
    12    try:
    13        return first_input / second_input
    14    except ZeroDivisionError:
    15        return 'brmph?! I cannot divide by 0. Try again...'
    16
    17
    18def add(first_input, second_input):
    

    the test passes


  • I change the assertRaises to assertEqual for the multiply function in test_calculator_raises_type_error_w_none in test_calculator.py

    62      self.assertEqual(
    63          src.calculator.divide(None, None),
    64          'brmph?! Numbers only. Try again...'
    65      )
    66      self.assertEqual(
    67          src.calculator.multiply(None, None),
    68          'brmph?! Numbers only. Try again...'
    69      )
    70      with self.assertRaises(TypeError):
    71          src.calculator.subtract(None, None)
    72
    73  def test_calculator_raises_type_error_w_strings(self):
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
    
  • I add an if statement to the multiply function in calculator.py

     5def multiply(first_input, second_input):
     6    if first_input is None or second_input is None:
     7        return 'brmph?! Numbers only. Try again...'
     8    return first_input * second_input
     9
    10
    11def divide(first_input, second_input):
    

    the test passes


  • I change the assertRaises for the subtract function to assertEqual in test_calculator_raises_type_error_w_none in test_calculator.py

    66        self.assertEqual(
    67            src.calculator.multiply(None, None),
    68            'brmph?! Numbers only. Try again...'
    69        )
    70        self.assertEqual(
    71            src.calculator.subtract(None, None),
    72            'brmph?! Numbers only. Try again...'
    73        )
    74
    75    def test_calculator_raises_type_error_w_strings(self):
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for -: 'NoneType' and 'NoneType'
    
  • I add an if statement to the subtract function in calculator.py

    1def subtract(first_input, second_input):
    2    if first_input is None or second_input is None:
    3        return 'brmph?! Numbers only. Try again...'
    4    return first_input - second_input
    5
    6
    7def multiply(first_input, second_input):
    

    the test passes


  • I add a variable

    57    def test_calculator_raises_type_error_w_none(self):
    58        error_message = 'brmph?! Numbers only. Try again...'
    59
    60        self.assertEqual(
    61            src.calculator.add(None, None),
    62            'brmph?! Numbers only. Try again...'
    63        )
    
  • I use it to remove the repetition of the error message

    57    def test_calculator_raises_type_error_w_none(self):
    58        error_message = 'brmph?! 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
    77    def test_calculator_raises_type_error_w_strings(self):
    

    the test is still green


what is a decorator function?

3 of the functions in the calculator program have the same if statement

if first_input is None or second_input is None:
    return 'brmph?! Numbers only. Try again...'

How can I remove this repetition? The only difference between the 3 functions is in what they do with the inputs

return first_input - second_input
return first_input * second_input
return first_input / second_input

I can use a decorator/wrapper function to remove the repetition from those 3 functions. It is a function that takes another function as input.

  • I add a decorator function to calculator.py

    1def input_is_not_none(function):
    2    def wrapper(first_input, second_input):
    3        if first_input is None or second_input is None:
    4            return 'brmph?! Numbers only. Try again...'
    5        return function(first_input, second_input)
    6    return wrapper
    7
    8
    9def subtract(first_input, second_input):
    

    The input_is_not_none function

  • I use it with the subtract function

     9@input_is_not_none
    10def subtract(first_input, second_input):
    11    if first_input is None or second_input is None:
    12        return 'brmph?! Numbers only. Try again...'
    13    return first_input - second_input
    

    the tests are still green

  • I remove the if statement from the subtract function

     9@input_is_not_none
    10def subtract(first_input, second_input):
    11    return first_input - second_input
    12
    13
    14def multiply(first_input, second_input):
    

    still green

  • I use input_is_not_none with the multiply function

    14@input_is_not_none
    15def multiply(first_input, second_input):
    16    if first_input is None or second_input is None:
    17        return 'brmph?! Numbers only. Try again...'
    18    return first_input * second_input
    

    green

  • I remove the if statement from the multiply function

    14@input_is_not_none
    15def multiply(first_input, second_input):
    16    return first_input * second_input
    17
    18
    19def divide(first_input, second_input):
    

    still green

  • I wrap the divide function with input_is_not_none

    19@input_is_not_none
    20def divide(first_input, second_input):
    21    if first_input is None or second_input is None:
    22        return 'brmph?! Numbers only. Try again...'
    23    try:
    24        return first_input / second_input
    25    except ZeroDivisionError:
    26        return 'brmph?! I cannot divide by 0. Try again...'
    

    the tests are still green

  • I remove the if statement

    19@input_is_not_none
    20def divide(first_input, second_input):
    21    try:
    22        return first_input / second_input
    23    except ZeroDivisionError:
    24        return 'brmph?! I cannot divide by 0. Try again...'
    25
    26
    27def add(first_input, second_input):
    

    still green

  • I try it with the add function

    27@input_is_not_none
    28def add(first_input, second_input):
    29    if (
    30        isinstance(first_input, str)
    31        or
    32        isinstance(second_input, str)
    33    ):
    34        raise TypeError
    35    else:
    36        try:
    37            return first_input + second_input
    38        except TypeError:
    39            return 'brmph?! Numbers only. Try again...'
    

    the tests are still green

  • I remove the exception handler from the add function

     1def input_is_not_none(function):
     2    def wrapper(first_input, second_input):
     3        if first_input is None or second_input is None:
     4            return 'brmph?! Numbers only. Try again...'
     5        return function(first_input, second_input)
     6    return wrapper
     7
     8
     9@input_is_not_none
    10def subtract(first_input, second_input):
    11    return first_input - second_input
    12
    13
    14@input_is_not_none
    15def multiply(first_input, second_input):
    16    return first_input * second_input
    17
    18
    19@input_is_not_none
    20def divide(first_input, second_input):
    21    try:
    22        return first_input / second_input
    23    except ZeroDivisionError:
    24        return 'brmph?! I cannot divide by 0. Try again...'
    25
    26
    27@input_is_not_none
    28def add(first_input, second_input):
    29    if (
    30        isinstance(first_input, str)
    31        or
    32        isinstance(second_input, str)
    33    ):
    34        raise TypeError
    35    else:
    36        return first_input + second_input
    

    still green


  • to continue with the goal that the calculator functions send a message when they gets something that is not a number, I change assertRaises to assertEqual in test_calculator_raises_type_error_w_strings for the add function in test_calculator.py

    77    def test_calculator_raises_type_error_w_strings(self):
    78        self.assertEqual(
    79            src.calculator.add('1', '1'),
    80            'brmph?! Numbers only! Try again...'
    81        )
    82        with self.assertRaises(TypeError):
    

    the terminal shows TypeError

    TypeError
    

    I could have added a message to the raise statement for that Exception

  • I change the raise statement to a return statement with a message in calculator.py

    27@input_is_not_none
    28def add(first_input, second_input):
    29    if (
    30        isinstance(first_input, str)
    31        or
    32        isinstance(second_input, str)
    33    ):
    34        return 'brmph?! Numbers only. Try again...'
    35    else:
    36        return first_input + second_input
    

    the test passes


  • I change assertRaises to assertEqual for the divide function in test_calculator.py

    77    def test_calculator_raises_type_error_w_strings(self):
    78        self.assertEqual(
    79            src.calculator.add('1', '1'),
    80            'brmph?! Numbers only. Try again...'
    81        )
    82        self.assertEqual(
    83            src.calculator.divide('1', '1'),
    84            'brmph?! Numbers only. Try again...'
    85        )
    86        with self.assertRaises(TypeError):
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for /: 'str' and 'str'
    
  • I add another except clause to the divide function in calculator.py

    19@input_is_not_none
    20def divide(first_input, second_input):
    21    try:
    22        return first_input / second_input
    23    except ZeroDivisionError:
    24        return 'brmph?! I cannot divide by 0. Try again...'
    25    except TypeError:
    26        return 'brmph?! Numbers only. Try again...'
    

    the test passes


  • I change assertRaises to assertEqual for the multiply function in test_calculator_raises_type_error_w_strings in test_calculator.py

    77    def test_calculator_raises_type_error_w_strings(self):
    78        self.assertEqual(
    79            src.calculator.add('1', '1'),
    80            'brmph?! Numbers only. Try again...'
    81        )
    82        self.assertEqual(
    83            src.calculator.divide('1', '1'),
    84            'brmph?! Numbers only. Try again...'
    85        )
    86        self.assertEqual(
    87            src.calculator.multiply('1', '1'),
    88            'brmph?! Numbers only. Try again...'
    89        )
    90        with self.assertRaises(TypeError):
    91            src.calculator.subtract('1', '1')
    

    the terminal shows TypeError

    TypeError: can't multiply sequence by non-int of type 'str'
    
  • I add an exception handler to the multiply function in calculator.py

    14@input_is_not_none
    15def multiply(first_input, second_input):
    16    try:
    17        return first_input * second_input
    18    except TypeError:
    19        return 'brmph?! Numbers only. Try again...'
    

    the test passes


  • I change the assertRaises to assertEqual for the subtract function in test_calculator.py

    77    def test_calculator_raises_type_error_w_strings(self):
    78        self.assertEqual(
    79            src.calculator.add('1', '1'),
    80            'brmph?! Numbers only. Try again...'
    81        )
    82        self.assertEqual(
    83            src.calculator.divide('1', '1'),
    84            'brmph?! Numbers only. Try again...'
    85        )
    86        self.assertEqual(
    87            src.calculator.multiply('1', '1'),
    88            'brmph?! Numbers only. Try again...'
    89        )
    90        self.assertEqual(
    91            src.calculator.subtract('1', '1'),
    92            'brmph?! Numbers only. Try again...'
    93        )
    94
    95
    96# Exceptions seen
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for -: 'str' and 'str'
    
  • I add an exception handler to the subtract function in calculator.py

     9@input_is_not_none
    10def subtract(first_input, second_input):
    11    try:
    12        return first_input - second_input
    13    except TypeError:
    14        return 'brmph?! Numbers only. Try again...'
    

    the test passes


3 of the functions in the calculator program have the same exception handler

try:
    something
except TypeError:
    return 'brmph?! Numbers only. Try again...'

the divide function is different because it has another except clause

except ZeroDivisionError:
    return 'brmph?! I cannot divide by 0. Try again...'

I can use a decorator/wrapper function to remove the repetition of the exception handler from the functions

  • I add a new decorator function to calculator.py

     1def type_error_handler(function):
     2    def handler(first_input, second_input):
     3        try:
     4            return function(first_input, second_input)
     5        except TypeError:
     6            return 'brmph?! Numbers only. Try again...'
     7    return handler
     8
     9
    10def input_is_not_none(function):
    

    The type_error_handler function

  • I use it to wrap the subtract function

    18@type_error_handler
    19@input_is_not_none
    20def subtract(first_input, second_input):
    21    try:
    22        return first_input - second_input
    23    except TypeError:
    24        return 'brmph?! Numbers only. Try again...'
    

    the tests are still green

  • I remove the exception handler from the subtract function

    18@type_error_handler
    19@input_is_not_none
    20def subtract(first_input, second_input):
    21    return first_input - second_input
    22
    23
    24@input_is_not_none
    25def multiply(first_input, second_input):
    

    still green

  • I wrap the multiply function

    24@type_error_handler
    25@input_is_not_none
    26def multiply(first_input, second_input):
    27    try:
    28        return first_input * second_input
    29    except TypeError:
    30        return 'brmph?! Numbers only. Try again...'
    

    green

  • I remove the exception handler from the multiply function

    24@type_error_handler
    25@input_is_not_none
    26def multiply(first_input, second_input):
    27    return first_input * second_input
    28
    29
    30@input_is_not_none
    31def divide(first_input, second_input):
    

    still green

  • I wrap the divide function

    30@type_error_handler
    31@input_is_not_none
    32def divide(first_input, second_input):
    33    try:
    34        return first_input / second_input
    35    except ZeroDivisionError:
    36        return 'brmph?! I cannot divide by 0. Try again...'
    37    except TypeError:
    38        return 'brmph?! Numbers only. Try again...'
    

    the test is still green

  • I remove the second except clause

    30@type_error_handler
    31@input_is_not_none
    32def divide(first_input, second_input):
    33    try:
    34        return first_input / second_input
    35    except ZeroDivisionError:
    36        return 'brmph?! I cannot divide by 0. Try again...'
    37
    38
    39@input_is_not_none
    40def add(first_input, second_input):
    

    still green


  • The two decorator functions both return the result of calling the given functions or an error message

    return function(first_input, second_input)
    return 'brmph?! Numbers only. Try again...'
    

    I make a new decorator to do the work of type_error_handler and input_is_not_none

     1def numbers_only(function):
     2    def decorator(first_input, second_input):
     3        error_message = 'brmph?! Numbers only. Try again...'
     4        if first_input is None or second_input is None:
     5            return error_message
     6        else:
     7            try:
     8                return function(first_input, second_input)
     9            except TypeError:
    10                return error_message
    11    return decorator
    12
    13
    14def type_error_handler(function):
    

    The numbers_only function

  • I use numbers_only to wrap the subtract function

    31@numbers_only
    32@type_error_handler
    33@input_is_not_none
    34def subtract(first_input, second_input):
    35    return first_input - second_input
    

    the tests are still green

  • I remove the other wrappers from the the subtract function

    76@numbers_only
    77def subtract(first_input, second_input):
    78    return first_input - second_input
    79
    80
    81@type_error_handler
    82@input_is_not_none
    83def multiply(first_input, second_input):
    

    still green

  • I wrap the multiply function

    36@numbers_only
    37@type_error_handler
    38@input_is_not_none
    39def multiply(first_input, second_input):
    40    return first_input * second_input
    

    green

  • I remove the other wrappers from the multiply function

    44@numbers_only
    45def multiply(first_input, second_input):
    46    return first_input * second_input
    47
    48
    49@type_error_handler
    50@input_is_not_none
    51def divide(first_input, second_input):
    

    still green

  • I wrap the divide function

    41@numbers_only
    42@type_error_handler
    43@input_is_not_none
    44def divide(first_input, second_input):
    45    try:
    46        return first_input / second_input
    47    except ZeroDivisionError:
    48        return 'brmph?! I cannot divide by 0. Try again...'
    

    the tests are still green

  • I remove the other wrappers from the divide function

    41@numbers_only
    42def divide(first_input, second_input):
    43    try:
    44        return first_input / second_input
    45    except ZeroDivisionError:
    46        return 'brmph?! I cannot divide by 0. Try again...'
    47
    48
    49@type_error_handler
    50@input_is_not_none
    51def add(first_input, second_input):
    

    still green

  • I wrap the add function

    49@numbers_only
    50@type_error_handler
    51@input_is_not_none
    52def add(first_input, second_input):
    53    if (
    54        isinstance(first_input, str)
    55        or
    56        isinstance(second_input, str)
    57    ):
    58        return 'brmph?! Numbers only. Try again...'
    59    else:
    60        return first_input + second_input
    

    green

  • I remove the other wrappers from the add function

    41@numbers_only
    42def add(first_input, second_input):
    43    if (
    44        isinstance(first_input, str)
    45        or
    46        isinstance(second_input, str)
    47    ):
    48        return 'brmph?! Numbers only. Try again...'
    49    else:
    50        return first_input + second_input
    

    still green

  • I remove type_error_handler and input_is_not_none because they are no longer used

     1def numbers_only(function):
     2    def decorator(first_input, second_input):
     3        error_message = 'brmph?! Numbers only. Try again...'
     4        if first_input is None or second_input is None:
     5            return error_message
     6        else:
     7            try:
     8                return function(first_input, second_input)
     9            except TypeError:
    10                return error_message
    11    return decorator
    12
    13
    14@numbers_only
    15def subtract(first_input, second_input):
    

    all tests are still green


  • I add a variable to test_calculator_raises_type_error_w_strings

    77    def test_calculator_raises_type_error_w_strings(self):
    78        error_message = 'brmph?! Numbers only. Try again...'
    79
    80        self.assertEqual(
    81            src.calculator.add('1', '1'),
    82            'brmph?! Numbers only. Try again...'
    83        )
    
  • I use the variable to remove the repetition of the error message

    77    def test_calculator_raises_type_error_w_strings(self):
    78        error_message = 'brmph?! Numbers only. Try again...'
    79
    80        self.assertEqual(
    81            src.calculator.add('1', '1'),
    82            error_message
    83        )
    84        self.assertEqual(
    85            src.calculator.divide('1', '1'),
    86            error_message
    87        )
    88        self.assertEqual(
    89            src.calculator.multiply('1', '1'),
    90            error_message
    91        )
    92        self.assertEqual(
    93            src.calculator.subtract('1', '1'),
    94            error_message
    95        )
    96
    97
    98# Exceptions seen
    

    still green

  • I remove the name of test_calculator_raises_type_error_w_strings to make its assertions part of test_calculator_raises_type_error_w_none

    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
    77        error_message = 'brmph?! Numbers only. Try again...'
    78
    79        self.assertEqual(
    80            src.calculator.add('1', '1'),
    81            error_message
    82        )
    83        self.assertEqual(
    84            src.calculator.divide('1', '1'),
    85            error_message
    86        )
    
  • I remove the repetition of the error_message variable

    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(
    
  • I change the name from test_calculator_raises_type_error_w_none to test_calculator_sends_message_when_input_is_not_a_number to be clearer

    57    def test_calculator_sends_message_when_input_is_not_a_number(self):
    58        error_message = 'brmph?! 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        )
    92
    93
    94# Exceptions seen
    95# AssertionError
    96# NameError
    97# AttributeError
    98# TypeError
    

    the tests are still green. All these assertions look the same, they check that the calculator functions return an error message if 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, especially since I want to test the other data types - booleans, tuples, lists and dictionaries. I do not think I am ready to write 4 assertions for each one.


  • I can make a function for the condition in the if statement in the add function in calculator.py

    24@numbers_only
    25def divide(first_input, second_input):
    26    try:
    27        return first_input / second_input
    28    except ZeroDivisionError:
    29        return 'brmph?! I cannot divide by 0. Try again...'
    30
    31
    32def is_string(something):
    33    return isinstance(something, str)
    34
    35
    36@numbers_only
    37def add(first_input, second_input):
    
  • then use is_string in the add function

    36@numbers_only
    37def add(first_input, second_input):
    38    # if (
    39    #     isinstance(first_input, str)
    40    #     or
    41    #     isinstance(second_input, str)
    42    # ):
    43    if is_string(first_input) or is_string(second_input):
    44        return 'brmph?! Numbers only. Try again...'
    45    else:
    46        return first_input + second_input
    

    the test is still green. This removes the duplication of str in the call to the isinstance function

    isinstance(first_input, str)
    isinstance(second_input, str)
    

    it also adds 2 lines of code to remove 6 characters. WOW!

  • I undo the change

  • I can make a function for the if statement in the add function

    24@numbers_only
    25def divide(first_input, second_input):
    26    try:
    27        return first_input / second_input
    28    except ZeroDivisionError:
    29        return 'brmph?! I cannot divide by 0. Try again...'
    30
    31
    32def one_input_is_a_string(first_input, second_input):
    33    return (
    34        isinstance(first_input, str)
    35        or
    36        isinstance(second_input, str)
    37    )
    38
    39
    40@numbers_only
    41def add(first_input, second_input):
    
  • then use one_input_is_a_string in the add function

    40@numbers_only
    41def add(first_input, second_input):
    42    # if (
    43    #     isinstance(first_input, str)
    44    #     or
    45    #     isinstance(second_input, str)
    46    # ):
    47    if one_input_is_a_string(first_input, second_input):
    48        return 'brmph?! Numbers only. Try again...'
    49    else:
    50        return first_input + second_input
    

    the test is still green. the one_input_is_a_string function

  • enough experiments for now. I undo the change because I do not need it

     1def numbers_only(function):
     2    def decorator(first_input, second_input):
     3        error_message = 'brmph?! Numbers only. Try again...'
     4        if first_input is None or second_input is None:
     5            return error_message
     6        else:
     7            try:
     8                return function(first_input, second_input)
     9            except TypeError:
    10                return error_message
    11    return decorator
    12
    13
    14@numbers_only
    15def subtract(first_input, second_input):
    16    return first_input - second_input
    17
    18
    19@numbers_only
    20def multiply(first_input, second_input):
    21    return first_input * second_input
    22
    23
    24@numbers_only
    25def divide(first_input, second_input):
    26    try:
    27        return first_input / second_input
    28    except ZeroDivisionError:
    29        return 'brmph?! I cannot divide by 0. Try again...'
    30
    31
    32@numbers_only
    33def add(first_input, second_input):
    34    if (
    35        isinstance(first_input, str)
    36        or
    37        isinstance(second_input, str)
    38    ):
    39        return 'brmph?! Numbers only. Try again...'
    40    else:
    41        return first_input + second_input
    

    and all the tests are still passing. The world is my oyster!


close the project

  • I close test_calculator.py and calculator.py in the editor

  • I click in the terminal, then use q on the keyboard to leave the tests. The terminal goes back to the command line

  • I change directory to the parent of calculator

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

The calculator program can take 2 inputs and check if they are both numbers, then add, subtract, multiply or divide them

Even though the program says it only works with numbers, I did not add tests for tuples, lists, sets, and dictionaries, though they are touched in test_type_error_w_objects_that_do_not_mix, Do you want to add them or do we already have enough tests to know what would happen?


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 test Lists?


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