TypeError


what causes TypeError?

TypeError is raised when an object is used in a way that it should not be. This helps understand how to use functions and classes


preview

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

 1import unittest
 2import src.type_error
 3
 4
 5class TestTypeError(unittest.TestCase):
 6
 7    def test_type_error_w_the_uncallables(self):
 8        src.type_error.none()
 9        src.type_error.false()
10        src.type_error.true()
11        src.type_error.a_list()
12        src.type_error.a_dictionary()
13
14    def test_type_error_w_function_signatures(self):
15        src.type_error.function_00('a')
16        src.type_error.function_01('a', 'b')
17        src.type_error.function_02('a', 'b', 'c')
18        src.type_error.function_03('a', 'b', 'c', 'd')
19
20    def test_type_error_w_objects_that_do_not_mix(self):
21        with self.assertRaises(TypeError):
22            None + 1
23        with self.assertRaises(TypeError):
24            'text' + 0.1
25        with self.assertRaises(TypeError):
26            (1, 2, 3, 'n') - {1, 2, 3, 'n'}
27
28
29# Exceptions seen
30# AssertionError
31# AttributeError
32# TypeError

start the project

  • I open a terminal

  • I type pwd to make sure I am in the pumping_python folder

    pwd
    

    the terminal goes back to the command line

    .../pumping_python
    

    Note

    if you are not in the pumping_python folder, try cd ~/pumping_python

  • I choose type_error as the name of this project

  • I open makePythonTdd.sh or makePythonTdd.ps1 in the editor

    Tip

    Here is a quick way to open makePythonTdd.sh or makePythonTdd.ps1 if you are using Visual Studio Code

    code makePythonTdd.sh
    

    on Windows without Windows Subsystem for Linux use

    code makePythonTdd.ps1
    
  • I change everywhere I have exceptions to the name of this project in makePythonTdd.sh

     1#!/bin/bash
     2mkdir type_error
     3cd type_error
     4mkdir src
     5touch src/type_error.py
     6mkdir tests
     7touch tests/__init__.py
     8
     9echo "import unittest
    10
    11
    12class TestTypeError(unittest.TestCase):
    13
    14    def test_failure(self):
    15        self.assertFalse(True)
    16
    17
    18# Exceptions seen
    19# AssertionError
    20" > tests/test_type_error.py
    

    on Windows without Windows Subsystem for Linux use makePythonTdd.ps1 instead of makePythonTdd.sh

     1mkdir type_error
     2cd type_error
     3mkdir src
     4New-Item src/type_error.py
     5mkdir tests
     6New-Item tests/__init__.py
     7
     8"import unittest
     9
    10
    11class TestTypeError(unittest.TestCase):
    12
    13    def test_failure(self):
    14        self.assertFalse(True)
    15
    16# Exceptions seen
    17# AssertionError
    18" | Out-File tests/test_type_error.py
    
  • I run the program in the terminal

    ./makePythonTdd.sh
    

    on Windows without Windows Subsystem for Linux use makePythonTdd.ps1 instead of makePythonTdd.sh

    ./makePythonTdd.ps1
    

    the terminal shows AssertionError

    ======================================= FAILURES =======================================
    _____________________________ TestTypeError.test_failure ______________________________
    
    self = <tests.test_exceptions.TestTypeError testMethod=test_failure>
    
        def test_failure(self):
    >       self.assertFalse(True)
    E       AssertionError: True is not false
    
    tests/test_type_error.py:7: AssertionError
    =============================== short test summary info ================================
    FAILED tests/test_type_error.py::TestMagic::test_failure - AssertionError: True is not false
    ================================== 1 failed in X.YZs ===================================
    
  • I hold ctrl (Windows/Linux) or option or command (MacOS) on the keyboard and use the mouse to click on tests/test_type_error.py:7 to open it in the editor

  • then I change True to False

    7        self.assertFalse(False)
    

    the test passes


test_type_error_w_the_uncallables

There are objects that can NOT be called

RED: make it fail

  • I add an import statement at the top of test_type_error.py

    1import src.type_error
    2import unittest
    
  • I change test_failure to test_type_error_w_the_uncallables with a failing line

    5class TestTypeError(unittest.TestCase):
    6
    7    def test_type_error_w_the_uncallables(self):
    8        src.type_error.none()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'none'
    

    there is nothing in type_error.py in the src folder yet

  • I add AttributeError to the list of Exceptions seen

    11# Exceptions seen
    12# AssertionError
    13# AttributeError
    

GREEN: make it pass

I can call a function, I cannot call None

REFACTOR: make it better

  • I add another failing line to test_type_error.py

    7    def test_type_error_w_the_uncallables(self):
    8        src.type_error.none()
    9        src.type_error.false()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'false'
    

    nothing is named false in type_error.py

  • I add the name to type_error.py and point it to False

    1def none():
    2    return None
    3
    4
    5false = False
    

    the terminal shows TypeError

    TypeError: 'bool' object is not callable
    

    I cannot call a boolean the way I can call a function

  • I change the variable to a function

    1def none():
    2    return None
    3
    4def false():
    5    return False
    

    the terminal shows green again

  • I add a line to test the other boolean in test_type_error.py

     7    def test_type_error_w_the_uncallables(self):
     8        src.type_error.none()
     9        src.type_error.false()
    10        src.type_error.true()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'true'
    

    there is nothing named true in type_error.py

  • I add the name and point it to True in type_error.py

    5def false():
    6    return False
    7
    8
    9true = True
    

    the terminal shows TypeError

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

     5def false():
     6    return False
     7
     8
     9def true():
    10    return True
    

    the test passes. I can call a function, I cannot call a boolean or None

  • I add a line for a string

     7    def test_type_error_w_the_uncallables(self):
     8        src.type_error.none()
     9        src.type_error.false()
    10        src.type_error.true()
    11        src.type_error.a_string()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'a_string'
    
  • I add the name and point it to a string in type_error.py

     9def true():
    10    return True
    11
    12
    13a_string = 'a string'
    

    the terminal shows TypeError

    TypeError: 'str' object is not callable
    
  • I change a_string to a function

     9def true():
    10    return True
    11
    12
    13def a_string():
    14    return 'a string'
    

    the test passes. I can call a function. I cannot call a string, a boolean or None

  • I add a failing line for a tuple (anything in parentheses (), separated by a comma)

     7    def test_type_error_w_the_uncallables(self):
     8        src.type_error.none()
     9        src.type_error.false()
    10        src.type_error.true()
    11        src.type_error.a_string()
    12        src.type_error.a_tuple()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'a_tuple'
    
  • I add the name and point it to a tuple

    13def a_string():
    14    return 'a string'
    15
    16
    17a_tuple = (1, 2, 3, 'n')
    

    the terminal shows TypeError

    TypeError: 'tuple' object is not callable
    
  • I change it to a function

    13def a_string():
    14    return 'a string'
    15
    16
    17def a_tuple():
    18    return (1, 2, 3, 'n')
    

    the test passes. I can call a function. I cannot call a tuple, string, boolean or None

  • I add another line to test_type_error.py

     7    def test_type_error_w_the_uncallables(self):
     8        src.type_error.none()
     9        src.type_error.false()
    10        src.type_error.true()
    11        src.type_error.a_string()
    12        src.type_error.a_tuple()
    13        src.type_error.a_list()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'a_list'
    
  • I add the name and point it to a list in type_error.py

    17def a_tuple():
    18    return (1, 2, 3, 'n')
    19
    20
    21a_list = [1, 2, 3, 'n']
    

    the terminal shows TypeError

    TypeError: 'list' object is not callable
    
  • I change a_list to a function

    17def a_tuple():
    18    return (1, 2, 3, 'n')
    19
    20
    21def a_list():
    22    return [1, 2, 3, 'n']
    

    the test passes. I can call a function, I cannot call a list, tuple, string, boolean or None

  • I add another failing line to test_type_error.py

    11        src.type_error.a_string()
    12        src.type_error.a_tuple()
    13        src.type_error.a_list()
    14        src.type_error.a_set()
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'a_set'
    
  • I add the name and point it to a set in type_error.py

    21def a_list():
    22    return [1, 2, 3, 'n']
    23
    24
    25a_set = {1, 2, 3, 'n'}
    

    the terminal shows TypeError

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

    21def a_list():
    22    return [1, 2, 3, 'n']
    23
    24
    25def a_set():
    26    return {1, 2, 3, 'n'}
    

    the test passes. I can call a function. I cannot call a set, list, tuple, string, boolean or None

  • I add the last failing line for this test to test_type_error.py

    12        src.type_error.a_tuple()
    13        src.type_error.a_list()
    14        src.type_error.a_set()
    15        src.type_error.a_dictionary()
    16
    17
    18# Exceptions seen
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'a_dictionary'
    
  • I add the name and point it to a dictionary in type_error.py

    25def a_set():
    26    return {1, 2, 3, 'n'}
    27
    28
    29a_dictionary = {'key': 'value'}
    

    the terminal shows TypeError

    TypeError: 'dict' object is not callable
    
  • I change it to a function

    13def a_set():
    14    return {1, 2, 3, 'n'}
    15
    16
    17def a_dictionary():
    18    return {'key': 'value'}
    

    the terminal shows green again. I can call a function. I cannot call a dictioanry, set, list, tuple, string, boolean or None

It is safe to say that I cannot call data structures because they are not callable. I can call functions, they are callable


test_type_error_w_function_signatures

When I call a function I have to match its definition also known as its signature or I get TypeError

RED: make it fail

  • I add a new test to test_type_error.py

    12        src.type_error.a_dictionary()
    13
    14    def test_type_error_w_function_signatures(self):
    15        src.type_error.function_00('a')
    16
    17
    18# Exceptions seen
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'function_00'
    

GREEN: make it pass

  • I add the function to type_error.py

    17def a_dictionary():
    18    return {'key': 'value'}
    19
    20
    21def function_00():
    22    return None
    

    the terminal shows TypeError

    TypeError: function_00() takes 0 positional arguments but 1 was given
    

    because function_00 is called with 'a' as input and the definition does not allow any inputs

  • I add a name in parentheses to the function definition

    21def function_00(the_input):
    22    return None
    

    the test passes

I have to call a function in a way that matches its definition or I get TypeError

REFACTOR: make it better

  • I add a new failing line to test_type_error.py

    14def test_type_error_w_function_signatures(self):
    15    src.type_error.function_00('a')
    16    src.type_error.function_01('a', 'b')
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'function_01'. Did you mean: 'function_00'?
    
  • I add the function to type_error.py

    21def function_00(the_input):
    22    return None
    23
    24
    25def function_01(the_input):
    26    return None
    

    the terminal shows TypeError

    TypeError: function_01() takes 1 positional argument but 2 were given
    

    the definition only allows one input, and the test sent two

  • I change the first name, then add another name in parentheses so that the call to the function and its definition match

    25def function_01(first, second):
    26  return None
    

    the test passes

  • I add another failing line to test_type_error.py

    14    def test_type_error_w_function_signatures(self):
    15        src.type_error.function_00('a')
    16        src.type_error.function_01('a', 'b')
    17        src.type_error.function_02('a', 'b', 'c')
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'function_02'. Did you mean: 'function_00'?
    
  • I add the function to type_error.py

    25def function_01(first, second):
    26    return None
    27
    28
    29def function_02(first, second):
    30    return None
    

    the terminal shows TypeError

    TypeError: function_02() takes 2 positional arguments but 3 were given
    
  • I change the name of the first input, then add another name in parentheses to make the number of inputs match in type_error.py

    29def function_02(first, second, third):
    30    return None
    

    the test passes

  • I add one more failing line in test_type_error.py

    14    def test_type_error_w_function_signatures(self):
    15        src.type_error.function_00('a')
    16        src.type_error.function_01('a', 'b')
    17        src.type_error.function_02('a', 'b', 'c')
    18        src.type_error.function_03('a', 'b', 'c', 'd')
    

    the terminal shows AttributeError

    AttributeError: module 'src.type_error' has no attribute 'function_03'. Did you mean: 'function_00'?
    
  • I add the function to type_error.py

    29def function_02(first, second, third):
    30    return None
    31
    32
    33def function_03(first, second, third):
    34    return None
    

    the terminal shows TypeError

    TypeError: function_03() takes 3 positional arguments but 4 were given
    
  • I add a 4th name in parentheses to the definition

    33def function_03(first, second, third, fourth):
    34    return None
    

    the test passes

I have to call a function with the same number of inputs its definition expects


test_type_error_w_objects_that_do_not_mix

Some operations do not work if the objects are not of the same type

RED: make it fail

I add a new test with a failing line in test_type_error.py

18        src.type_error.function_03('a', 'b', 'c', 'd')
19
20    def test_type_error_w_objects_that_do_not_mix(self):
21        None + 1
22
23
24# Exceptions seen

the terminal shows TypeError

TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

I cannot do arithmetic with None

GREEN: make it pass

I add the assertRaises method

20    def test_type_error_w_objects_that_do_not_mix(self):
21        with self.assertRaises(TypeError):
22            None + 1

the test passes

REFACTOR: make it better

  • I add another failing line to the test

    20    def test_type_error_w_objects_that_do_not_mix(self):
    21        with self.assertRaises(TypeError):
    22            None + 1
    23        'text' + 0.1
    

    the terminal shows TypeError

    TypeError: can only concatenate str (not "float") to str
    

    I cannot add something that is not a string to a string

  • I use assertRaises to handle the Exception

    20    def test_type_error_w_objects_that_do_not_mix(self):
    21        with self.assertRaises(TypeError):
    22            None + 1
    23        with self.assertRaises(TypeError):
    24            'text' + 0.1
    

    the test passes

  • I add another failing line

    20    def test_type_error_w_objects_that_do_not_mix(self):
    21        with self.assertRaises(TypeError):
    22            None + 1
    23        with self.assertRaises(TypeError):
    24            'text' + 0.1
    25        (1, 2, 3, 'n') - {1, 2, 3, 'n'}
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for -: 'tuple' and 'set'
    

    I add assertRaises

    20    def test_type_error_w_objects_that_do_not_mix(self):
    21        with self.assertRaises(TypeError):
    22            None + 1
    23        with self.assertRaises(TypeError):
    24            'text' + 0.1
    25        with self.assertRaises(TypeError):
    26            (1, 2, 3, 'n') - {1, 2, 3, 'n'}
    27
    28# Exceptions seen
    

    the terminal shows all tests are passing


close the project


test_calculator_raises_type_error

I want to use TypeError with exception handlers to make sure that the calculator program only works with numbers. Calculators in the real world only work with numbers

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 activate the virtual environment

    source .venv/bin/activate
    

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

    .venv/scripts/activate.ps1
    

    the terminal shows

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

    pytest-watch
    

    the 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.py to 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_when_given_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_when_given_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

    44    def test_calculator_raises_type_error_when_given_none(self):
    45        with self.assertRaises(TypeError):
    46            src.calculator.add(None None)
    47        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_when_given_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 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_when_given_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')
    
  • 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

    57    def test_calculator_raises_type_error_when_given_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('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

  • I change the assertEqual in test_calculator_with_strings to assertRaises in test_calculator.py

    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 isinstance(first_input, str) or isinstance(second_input, str):
    18        raise TypeError(
    19            'Excuse me?! I only work with numbers, Please try again...'
    20        )
    21    else:
    22        return first_input + second_input
    

    the test passes

    Note

  • I change the name of the test to be clearer

    67    def test_calculator_raises_type_error_when_given_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_when_given_none

57    def test_calculator_raises_type_error_when_given_none(self):
58        self.assertEqual(
59            src.calculator.add(None, None),
60            'Excuse me?! I only work with numbers, Please 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 isinstance(first_input, str) or isinstance(second_input, str):
18        raise TypeError(
19            'Excuse me?! I only work with numbers, Please try again...'
20        )
21    else:
22        try:
23            return first_input + second_input
24        except TypeError:
25            return 'Excuse me?! I only work with numbers, Please 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_when_given_strings in test_calculator.py

    70    def test_calculator_raises_type_error_when_given_strings(self):
    71        self.assertEqual(
    72            src.calculator.add('1', '1'),
    73            'Excuse me?! I only work with numbers, Please try again...'
    74        )
    75        with self.assertRaises(TypeError):
    76            src.calculator.divide('1', '1')
    

    the terminal shows TypeError

    TypeError: Excuse me?! I only work with numbers, Please try again...
    
  • I change the raise statement to a return statement in the add function in calculator.py

    16def add(first_input, second_input):
    17    if isinstance(first_input, str) or isinstance(second_input, str):
    18        return 'Excuse me?! I only work with numbers, Please try again...'
    19    else:
    20        try:
    21            return first_input + second_input
    22        except TypeError:
    23            return 'Excuse me?! I only work with numbers, Please try again...'
    

    the test passes

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

    70    def test_calculator_raises_type_error_when_given_strings(self):
    71        self.assertEqual(
    72            src.calculator.add('1', '1'),
    73            'Excuse me?! I only work with numbers, Please try again...'
    74        )
    75        self.assertEqual(
    76            src.calculator.divide('1', '1'),
    77            'Excuse me?! I only work with numbers, Please try again...'
    78        )
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for /: 'str' and 'str'
    
  • 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 'undefined: I cannot divide by 0'
    14    except TypeError:
    15        return 'Excuse me?! I only work with numbers, Please try again...'
    

    the terminal shows AssertionError

    AssertionError: TypeError not raised
    

    test_calculator_raises_type_error_when_given_none fails because it expects TypeError when the inputs are not numbers

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

    57def test_calculator_raises_type_error_when_given_none(self):
    58    self.assertEqual(
    59        src.calculator.add(None, None),
    60        'Excuse me?! I only work with numbers, Please try again...'
    61    )
    62    self.assertEqual(
    63        src.calculator.divide(None, None),
    64        'Excuse me?! I only work with numbers, Please try again...'
    65    )
    

    the test passes

  • I change the assertRaises to assertEqual for the multiply function in test_calculator_raises_type_error_when_given_none

    62        self.assertEqual(
    63            src.calculator.divide(None, None),
    64            'Excuse me?! I only work with numbers, Please try again...'
    65        )
    66        self.assertEqual(
    67            src.calculator.multiply(None, None),
    68            'Excuse me?! I only work with numbers, Please try again...'
    69        )
    

    the terminal shows TypeError

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

    5def multiply(first_input, second_input):
    6    try:
    7        return first_input * second_input
    8    except TypeError:
    9        return 'Excuse me?! I only work with numbers, Please try again...'
    

    the terminal shows AssertionError

    AssertionError: TypeError not raised
    
  • I change the assertRaises to assertEqual for the multiply function in test_calculator_raises_type_error_when_given_strings in test_calculator.py

    79        self.assertEqual(
    80            src.calculator.divide('1', '1'),
    81            'Excuse me?! I only work with numbers, Please try again...'
    82        )
    83        self.assertEqual(
    84            src.calculator.multiply('1', '1'),
    85            'Excuse me?! I only work with numbers, Please try again...'
    86        )
    

    the test passes

  • I change the assertRaises to assertEqual for the subtract function in test_calculator_raises_type_error_when_given_strings

    74    def test_calculator_raises_type_error_when_given_strings(self):
    75        self.assertEqual(
    76            src.calculator.add('1', '1'),
    77            'Excuse me?! I only work with numbers, Please try again...'
    78        )
    79        self.assertEqual(
    80            src.calculator.divide('1', '1'),
    81            'Excuse me?! I only work with numbers, Please try again...'
    82        )
    83        self.assertEqual(
    84            src.calculator.multiply('1', '1'),
    85            'Excuse me?! I only work with numbers, Please try again...'
    86        )
    87        self.assertEqual(
    88            src.calculator.subtract('1', '1'),
    89            'Excuse me?! I only work with numbers, Please try again...'
    90        )
    91
    92
    93# 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

    1def subtract(first_input, second_input):
    2    try:
    3        return first_input - second_input
    4    except TypeError:
    5        return 'Excuse me?! I only work with numbers, Please try again...'
    

    the terminal shows AssertionError

    AssertionError: TypeError not raised
    
  • I change the assertRaises to assertEqual for the subtract function in test_calculator_raises_type_error_when_given_none in test_calculator.py

    57    def test_calculator_raises_type_error_when_given_none(self):
    58        self.assertEqual(
    59            src.calculator.add(None, None),
    60            'Excuse me?! I only work with numbers, Please try again...'
    61        )
    62        self.assertEqual(
    63            src.calculator.divide(None, None),
    64            'Excuse me?! I only work with numbers, Please try again...'
    65        )
    66        self.assertEqual(
    67            src.calculator.multiply(None, None),
    68            'Excuse me?! I only work with numbers, Please try again...'
    69        )
    70        self.assertEqual(
    71            src.calculator.subtract(None, None),
    72            'Excuse me?! I only work with numbers, Please try again...'
    73        )
    74
    75
    76    def test_calculator_raises_type_error_when_given_strings(self):
    

    the test passes

  • That was a lot of doing the same thing over and over again. test_calculator_raises_type_error_when_given_none and test_calculator_raises_type_error_when_given_strings both look the same and the calculator no longer raises TypeError when any of the inputs are NOT a number. I remove the name of test_calculator_raises_type_error_when_given_strings to make its assertions part of test_calculator_raises_type_error_when_given_none

    70        self.assertEqual(
    71            src.calculator.subtract(None, None),
    72            'Excuse me?! I only work with numbers, Please try again...'
    73        )
    74        self.assertEqual(
    75            src.calculator.add('1', '1'),
    76            'Excuse me?! I only work with numbers, Please try again...'
    77        )
    78        self.assertEqual(
    79            src.calculator.divide('1', '1'),
    80            'Excuse me?! I only work with numbers, Please try again...'
    81        )
    82        self.assertEqual(
    83            src.calculator.multiply('1', '1'),
    84            'Excuse me?! I only work with numbers, Please try again...'
    85        )
    86        self.assertEqual(
    87            src.calculator.subtract('1', '1'),
    88            'Excuse me?! I only work with numbers, Please try again...'
    89        )
    90
    91
    92# Exceptions seen
    
  • I change the name from test_calculator_raises_type_error_when_given_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        self.assertEqual(
    

    the tests are still green

  • I have the same error message 8 times in this test. I can 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?! I only work with numbers, Please try again...'
    59        self.assertEqual(
    60            src.calculator.add(None, None),
    61            error_message
    62        )
    63        self.assertEqual(
    64            src.calculator.divide(None, None),
    65            error_message
    66        )
    67        self.assertEqual(
    68            src.calculator.multiply(None, None),
    69            error_message
    70        )
    71        self.assertEqual(
    72            src.calculator.subtract(None, None),
    73            error_message
    74        )
    75        self.assertEqual(
    76            src.calculator.add('1', '1'),
    77            error_message
    78        )
    79        self.assertEqual(
    80            src.calculator.divide('1', '1'),
    81            error_message
    82        )
    83        self.assertEqual(
    84            src.calculator.multiply('1', '1'),
    85            error_message
    86        )
    87        self.assertEqual(
    88            src.calculator.subtract('1', '1'),
    89            error_message
    90        )
    

    still green. All these tests assertions the same

    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?! I only work with numbers, Please 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 /
return first_input + second_input

what is a decorator function?

A decorator or wrapper function takes another functions 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 to calculator.py

     1def 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?! I only work with numbers, Please try again...'
     7    return wrapper
     8
     9
    10def subtract(first_input, second_input):
    

    The only_takes_numbers function takes a function as input

    • it tries to return the result of the function working on the two inputs

    • if TypeError is raised it returns the error message

  • 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_numbers function

    15@only_takes_numbers
    16def subtract(first_input, second_input):
    17    return 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_numbers function

    15@only_takes_numbers
    16def multiply(first_input, second_input):
    17    return 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

    2@only_takes_numbers
    3def divide(first_input, second_input):
    4    try:
    5        return first_input / second_input
    6    except ZeroDivisionError:
    7        return 'undefined: I cannot divide by 0'
    

    all the tests are still green

  • one more to go, I wrap the add function with the only_takes_numbers Function definitions

    28@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?! I only work with numbers, Please try again...'
    32    else:
    33        return first_input + second_input
    

    green! Lovely!

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

    28def 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?! I only work with numbers, Please try again...'
    36    else:
    37        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 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?! I only work with numbers, Please try again...'
    36    else:
    37        return first_input + second_input
    

    the test is still green.

  • I can also make a decorator function for the if statement<if statements> to practice making them

     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?! I only work with numbers, Please 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_input
    

    the test is still green

  • the reject_strings and only_takes_numbers functions have parts that are the same

    def wrapper(first_input, second_input):
        ...
        return 'Excuse me?! I only work with numbers, Please try again...'
        ...
        return function(first_input, second_input)
    return wrapper
    

    I add the if statement to the only_takes_numbers function

    10def only_takes_numbers(function):
    11    def wrapper(first_input, second_input):
    12        if isinstance(first_input, str) or isinstance(second_input, str):
    13            return 'Excuse me?! I only work with numbers, Please try again...'
    14        else:
    15            try:
    16                return function(first_input, second_input)
    17            except TypeError:
    18                return 'Excuse me?! I only work with numbers, Please try again...'
    19    return wrapper
    

    the test is still green

  • I remove the reject_strings decoration from the add function

    37        return 'undefined: I cannot divide by 0'
    38
    39
    40@only_takes_numbers
    41def add(first_input, second_input):
    42    return first_input + second_input
    

    green

  • I remove the reject_strings function

    1def only_takes_numbers(function):
    

    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(s)

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

  • I deactivate the virtual environment

    deactivate
    

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

    .../pumping_python/calculator
    
  • 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, add, subtract, multiply and 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?

I ran tests for TypeError with


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?


please leave a review