how to make a calculator 10: part 4


open the project

  • I `change directory`_ to the calculator folder_

    cd calculator
    
  • I make a new test file_ for the Streamlit_ website

    touch tests/test_streamlit_calculator.py
    
  • I add streamlit_ to the requirements.txt file_

    echo "streamlit" >> requirements.txt
    

    Streamlit_ is a Python_ library that is used for making websites, it is not part of `The Python Standard Library`_

  • I install the `Python packages`_ that I wrote in the requirements file_

    uv add --requirement requirements.txt
    

    the terminal shows it installed the `Python packages`_

  • I use pytest-watcher to run the tests

    uv run pytest-watcher . --now
    

    the terminal_ shows

    rootdir: .../pumping_python/calculator
    configfile: pyproject.toml
    collected 8 items
    
    tests/test_calculator.py .....                                [ 62%]
    tests/test_calculator_website.py ...                          [100%]
    
    ======================== 5 passed in X.YZs =========================
    

test_streamlit_calculator_reset

I want the C and AC buttons to change the number the Calculator shows back to 0


RED: make it fail


I add a test

111      def test_streamlit_calculator_w_plus_minus(self):
112          a_number = '963.0258741'
113          for number in a_number:
114              self.tester.button(number).click().run()
115          self.assertEqual(
116              self.tester.session_state['number'], a_number
117          )
118
119          self.tester.button('+/-').click().run()
120          self.assertEqual(
121              self.tester.session_state['number'], f'-{a_number}'
122          )
123
124          self.tester.button('+/-').click().run()
125          self.assertEqual(
126              self.tester.session_state['number'], a_number
127          )
128
129      def test_streamlit_calculator_reset(self):
130          numbers = '123456789'
131
132          number = random.choice(numbers)
133          self.tester.button(number).click().run()
134          self.assertEqual(
135              self.tester.session_state['number'],
136              number
137          )
138
139          self.tester.button('C').click().run()
140          self.assertEqual(
141              self.tester.session_state['number'],
142              '0'
143          )
144
145
146  # Exceptions seen

the terminal_ shows AssertionError

AssertionError: 'C' != '0'
  • number = random.choice(numbers) picks a random number from '123456789' (the numbers variable)

  • self.tester.button(number).click().run() presses the random number

  • self.assertEqual(self.tester.session_state['number'], number) checks that the value of the number key in the `session state object`_ is the same as the number that was pressed

  • self.tester.button('C').click().run() presses the C button

  • self.assertEqual(self.tester.session_state['number'], '0') checks that the the number key in the `session state object`_ is set back to 0 after the C button is pressed


GREEN: make it pass


  • I add a function to streamlit_calculator.py

     4def show_state(display):
     5    display.write(streamlit.session_state['number'])
     6
     7
     8def reset():
     9    streamlit.session_state['number'] = '0'
    10
    11
    12def plus_minus():
    
  • I add the on_click and args parameters to the C button in the add_buttons_to_column_2 function

    65def add_buttons_to_column_2(column_2, display):
    66    column_2.button(
    67        'C', key='C', width='stretch', on_click=on_click,
    68        args=[reset, display], type='primary',
    69    )
    70    column_2.button(
    71        '8', key='8', width='stretch', on_click=on_click,
    72        args=[add_number_to_state, display, '8'],
    73    )
    74    column_2.button(
    75        '5', key='5', width='stretch', on_click=on_click,
    76        args=[add_number_to_state, display, '5'],
    77    )
    78    column_2.button(
    79        '2', key='2', width='stretch', on_click=on_click,
    80        args=[add_number_to_state, display, '2'],
    81    )
    82    column_2.button(
    83        '0', key='0', width='stretch', on_click=on_click,
    84        args=[add_number_to_state, display, '0'],
    85    )
    86
    87
    88def add_buttons_to_column_3(column_3, display):
    

    the test passes


REFACTOR: make it better


  • I refresh the browser, click on number buttons and when I click on C it clears the numbers I type

  • I add an assertion for the AC button in test_streamlit_calculator_reset

    129    def test_streamlit_calculator_reset(self):
    130        numbers = '123456789'
    131
    132        number = random.choice(numbers)
    133        self.tester.button(number).click().run()
    134        self.assertEqual(
    135            self.tester.session_state['number'],
    136            number
    137        )
    138
    139        self.tester.button('C').click().run()
    140        self.assertEqual(
    141            self.tester.session_state['number'],
    142            '0'
    143        )
    144
    145        number = random.choice(numbers)
    146        self.tester.button(number).click().run()
    147        self.assertEqual(
    148            self.tester.session_state['number'],
    149            number
    150        )
    151
    152        self.tester.button('AC').click().run()
    153        self.assertEqual(
    154            self.tester.session_state['number'],
    155            '0'
    156        )
    157
    158
    159# Exceptions seen
    

    the terminal_ shows AssertionError

    AssertionError: 'D' != '0'
    
  • I add the on_click and args parameters to the AC button in the add_buttons_to_column_3 function

     88def add_buttons_to_column_3(column_3, display):
     89    column_3.button(
     90        'AC', key='AC', width='stretch', on_click=on_click,
     91        args=[reset, display], type='primary',
     92    )
     93    column_3.button(
     94        '9', key='9', width='stretch', on_click=on_click,
     95        args=[add_number_to_state, display, '9'],
     96    )
     97    column_3.button(
     98        '6', key='6', width='stretch', on_click=on_click,
     99        args=[add_number_to_state, display, '6'],
    100    )
    101    column_3.button(
    102        '3', key='3', width='stretch', on_click=on_click,
    103        args=[add_number_to_state, display, '3'],
    104    )
    105    column_3.button(
    106        '.', key='.', width='stretch', on_click=on_click,
    107        args=[add_decimal, display],
    108    )
    109
    110
    111def add_buttons_to_column_4(column_4):
    

    the test passes

  • I refresh the browser, click on number buttons and when I click on AC it clears the numbers

On to the arithmetic operations


test_streamlit_calculator_operations


RED: make it fail


I add a test for a calculation in test_streamlit_calculator.py

152        self.tester.button('AC').click().run()
153        self.assertEqual(
154            self.tester.session_state['number'],
155            '0'
156        )
157
158    def test_streamlit_calculator_operations(self):
159        first_number = '1'
160        second_number = '2'
161
162        self.tester.button(first_number).click().run()
163        self.tester.button('+').click().run()
164        self.tester.button(second_number).click().run()
165        self.tester.button('=').click().run()
166
167        self.assertEqual(
168            self.tester.session_state['number'],
169            float(first_number)+float(second_number)
170        )
171
172
173# Exceptions seen

the terminal_ shows KeyError

KeyError: '+'

I forgot that I used r'\+' as the key for addition


GREEN: make it pass


  • I change the key in the test

    162        self.tester.button(first_number).click().run()
    163        self.tester.button(r'\+').click().run()
    164        self.tester.button(second_number).click().run()
    165        self.tester.button('=').click().run()
    
  • I add an assertion to check the value of the first number

    162        self.tester.button(first_number).click().run()
    163        self.tester.button('\+').click().run()
    164        self.assertEqual(
    165            self.tester.session_state['first_number'],
    166            first_number
    167        )
    168
    169        self.tester.button(second_number).click().run()
    170        self.tester.button('=').click().run()
    

    the terminal_ shows KeyError

    KeyError: 'st.session_state has no key "first_number". Did you forget to initialize it? More info: https://docs.streamlit.io/develop/concepts/architecture/session-state#initialization'
    
  • I make a function to add the first number as a key in the `session state object`_ when the + button is pressed, in streamlit_calculator.py

     8def reset():
     9    streamlit.session_state['number'] = '0'
    10
    11
    12def first_number():
    13    first_number = streamlit.session_state['number']
    14    streamlit.session_state['first_number'] = first_number
    15    reset()
    16
    17
    18def plus_minus():
    
  • I add the on_click and args parameters to the + button in the add_buttons_to_column_4 function

    116def add_buttons_to_column_4(column_4):
    117    column_4.button(
    118        '/', key='/', width='stretch', type='primary',
    119    )
    120    column_4.button(
    121        'X', key='X', width='stretch', type='primary',
    122    )
    123    column_4.button(
    124        r'\-', key=r'\-', width='stretch', type='primary',
    125    )
    126    column_4.button(
    127        r'\+', key=r'\+', width='stretch', on_click=on_click,
    128        args=[first_number], type='primary',
    129    )
    130    column_4.button(
    131        '=', key='=', width='stretch', type='primary',
    132    )
    

    the terminal_ shows KeyError again and TypeError

    TypeError: on_click() missing 1 required positional argument: 'display'
    
  • I add display to the args list

    126    column_4.button(
    127        r'\+', key=r'\+', width='stretch', on_click=on_click,
    128        args=[first_number, display], type='primary',
    129    )
    

    the terminal_ shows KeyError

  • I add display as a positional argument of the add_buttons_to_column_4 function

    116def add_buttons_to_column_4(column_4, display):
    

    the terminal_ shows AssertionError for more tests

  • I add display in the call to the add_buttons_to_column_4 function from the add_buttons function

    135def add_buttons():
    136    display = streamlit.container(border=True)
    137    column_1, column_2, column_3, operations = streamlit.columns(4)
    138
    139    add_buttons_to_column_1(column_1, display)
    140    add_buttons_to_column_2(column_2, display)
    141    add_buttons_to_column_3(column_3, display)
    142    add_buttons_to_column_4(operations, display)
    143
    144
    145def main():
    

    the terminal_ shows AssertionError

    AssertionError: '2' != 3.0
    
  • I add an assertion for the second number after the = button is pressed, in test_streamlit_calculator.py

    158    def test_streamlit_calculator_operations(self):
    159        first_number = '1'
    160        second_number = '2'
    161
    162        self.tester.button(first_number).click().run()
    163        self.tester.button('\+').click().run()
    164        self.assertEqual(
    165            self.tester.session_state['first_number'],
    166            first_number
    167        )
    168
    169        self.tester.button(second_number).click().run()
    170        self.tester.button('=').click().run()
    171        self.assertEqual(
    172            self.tester.session_state['second_number'],
    173            second_number
    174        )
    175
    176        self.assertEqual(
    177            self.tester.session_state['number'],
    178            float(first_number)+float(second_number)
    179        )
    

    the terminal_ shows KeyError

    KeyError: 'st.session_state has no key "second_number". Did you forget to initialize it? More info: https://docs.streamlit.io/develop/concepts/architecture/session-state#initialization'
    
  • I make a new function for the result of the calculation, in streamlit_calculator.py

    12def first_number():
    13    first_number = streamlit.session_state['number']
    14    streamlit.session_state['first_number'] = first_number
    15    reset()
    16
    17
    18def calculate():
    19    second_number = streamlit.session_state['number']
    20    streamlit.session_state['second_number'] = second_number
    21    reset()
    22
    23
    24def plus_minus():
    
  • I add the on_click and args parameters to the = button in the add_buttons_to_column_4 function

    121def add_buttons_to_column_4(column_4, display):
    122    column_4.button(
    123        '/', key='/', width='stretch', type='primary',
    124    )
    125    column_4.button(
    126        'X', key='X', width='stretch', type='primary',
    127    )
    128    column_4.button(
    129        r'\-', key=r'\-', width='stretch', type='primary',
    130    )
    131    column_4.button(
    132        r'\+', key=r'\+', width='stretch', on_click=on_click,
    133        args=[first_number, display], type='primary',
    134    )
    135    column_4.button(
    136        '=', key='=', width='stretch', on_click=on_click,
    137        args=[calculate, display], type='primary',
    138    )
    

    the terminal_ shows AssertionError

    AssertionError: '0' != 3.0
    
  • I add a calculation to the calculate function

    18def calculate():
    19    second_number = streamlit.session_state['number']
    20    streamlit.session_state['second_number'] = second_number
    21    streamlit.session_state['number'] = (
    22        streamlit.session_state['first_number']
    23      + streamlit.session_state['second_number']
    24    )
    

    the terminal_ shows AssertionError

    AssertionError: '12' != 3.0
    
  • I change the numbers to floats_

     4def calculate():
     5    second_number = streamlit.session_state['number']
     6    streamlit.session_state['second_number'] = second_number
     7    streamlit.session_state['number'] = (
     8        float(streamlit.session_state['first_number'])
     9      + float(streamlit.session_state['second_number'])
    10    )
    

    the test passes

  • I refresh the browser and try the same calculation

    Addition Result

    the result is correct but the number looks different from the others

  • when I try another number, the browser shows TypeError

    TypeError after Addition

    the terminal_ for the browser also shows TypeError

    TypeError: unsupported operand type(s) for +=: 'float' and 'str'
    

REFACTOR: make it better


  • I want the result to look the same as the other numbers. I change it to a string_ in the calculate function

    the terminal_ shows AssertionError

    AssertionError: '3.0' != 3.0
    
  • I change the expectation of the assertion in test_streamlit_calculator_operations in test_streamlit_calculator.py

    176        self.assertEqual(
    177            self.tester.session_state['number'],
    178            str(float(first_number)+float(second_number))
    179        )
    

    the test passes

  • I refresh the browser and try the calculation again

    Addition with String Result

    I like it, though I do not need the .0 after the 3

  • I add an assertion for subtraction

    176        self.assertEqual(
    177            self.tester.session_state['number'],
    178            str(float(first_number)+float(second_number))
    179        )
    180
    181        self.tester.button(first_number).click().run()
    182        self.tester.button('\-').click().run()
    183        self.tester.button(second_number).click().run()
    184        self.tester.button('=').click().run()
    185        self.assertEqual(
    186            self.tester.session_state['number'],
    187            str(float(first_number)-float(second_number))
    188        )
    189
    190
    191# Exceptions seen
    

    the terminal_ shows AssertionError

    AssertionError: '4.0120000000000005' != '-1.0'
    

    I need to handle the operations

  • I add an assertion for an 'operation' key in the `session state object`_

    158    def test_streamlit_calculator_operations(self):
    159        first_number = '1'
    160        second_number = '2'
    161
    162        operation = '\+'
    163        self.tester.button(first_number).click().run()
    164        self.tester.button(operation).click().run()
    165        self.assertEqual(
    166            self.tester.session_state['first_number'],
    167            first_number
    168        )
    169        self.assertEqual(
    170            self.tester.session_state['operation'],
    171            operation
    172        )
    

    the terminal_ shows KeyError

    KeyError: 'st.session_state has no key "operation".
    Did you forget to initialize it?
    More info: https://docs.streamlit.io/develop/concepts/architecture/session-state#initialization'
    
  • I add a key to the first_number function in streamlit_calculator.py

    12def first_number(operation):
    13    first_number = streamlit.session_state['number']
    14    streamlit.session_state['first_number'] = first_number
    15    streamlit.session_state['operation'] = operation
    16    reset()
    

    the terminal_ still shows KeyError

  • I add the value for operation to the args list for the r'\+' button in the add_buttons_to_column_4 function

    137    column_4.button(
    138        r'\+', key=r'\+', width='stretch', on_click=on_click,
    139        args=[first_number, display, r'\+'], type='primary',
    140    )
    

    the terminal_ shows AssertionError

    AssertionError: '4.0120000000000005' != '-1.0'
    
  • I add an assertion for the operation key after the - button is pressed, in test_streamlit_calculator.py

    158    def test_streamlit_calculator_operations(self):
    159        first_number = '1'
    160        second_number = '2'
    161
    162        operation = '\+'
    163        self.tester.button(first_number).click().run()
    164        self.tester.button(operation).click().run()
    165        self.assertEqual(
    166            self.tester.session_state['first_number'],
    167            first_number
    168        )
    169        self.assertEqual(
    170            self.tester.session_state['operation'],
    171            operation
    172        )
    173
    174        self.tester.button(second_number).click().run()
    175        self.tester.button('=').click().run()
    176        self.assertEqual(
    177            self.tester.session_state['second_number'],
    178            second_number
    179        )
    180
    181        self.assertEqual(
    182            self.tester.session_state['number'],
    183            str(float(first_number)+float(second_number))
    184        )
    185
    186        operation = r'\-'
    187        self.tester.button('AC').click().run()
    188        self.tester.button(first_number).click().run()
    189        self.tester.button(operation).click().run()
    190        self.tester.button(second_number).click().run()
    191        self.tester.button('=').click().run()
    192        self.assertEqual(
    193            self.tester.session_state['operation'],
    194            operation
    195        )
    196        self.assertEqual(
    197            self.tester.session_state['number'],
    198            str(float(first_number)-float(second_number))
    199        )
    200
    201
    202# Exceptions seen
    

    the terminal_ shows AssertionError

    AssertionError: '\\+' != '\\-'
    
  • I use the operation in a dictionary in the calculate function in streamlit_calculator.py

    19def calculate():
    20    arithmetic = {
    21        r'\+': calculator.add,
    22    }
    23    second_number = streamlit.session_state['number']
    24    streamlit.session_state['second_number'] = second_number
    25    streamlit.session_state['number'] = str(
    26        float(streamlit.session_state['first_number'])
    27      + float(streamlit.session_state['second_number'])
    28    )
    

    the terminal_ shows KeyError

    KeyError: 'st.session_state has no key "second_number".
    Did you forget to initialize it?
    More info: https://docs.streamlit.io/develop/concepts/architecture/session-state#initialization'
    

    the terminal_ also shows NameError

    NameError: name 'calculator' is not defined. Did you mean: 'calculate'?
    
  • I add an `import statement`_ for the calculator module

    1import calculator
    2import streamlit
    3
    4
    5def show_state(display):
    

    the terminal_ shows AssertionError

    AssertionError: '\\+' != '\\-'
    
  • I add the on_click and args parameters to the - button in the add_buttons_to_column_4 function

    138    column_4.button(
    139        r'\-', key=r'\-', width='stretch', on_click=on_click,
    140        args=[first_number, display, r'\-'], type='primary',
    141    )
    

    the terminal_ shows AssertionError

    AssertionError: '5.01' != '-1.0'
    
  • I add the operation for subtraction to the calculate function

    20def calculate():
    21    arithmetic = {
    22        r'\+': calculator.add,
    23        r'\-': calculator.subtract,
    24    }
    
  • I use the operation for the calculation

    20def calculate():
    21    arithmetic = {
    22        r'\+': calculator.add,
    23        r'\-': calculator.subtract,
    24    }
    25    second_number = streamlit.session_state['number']
    26    streamlit.session_state['second_number'] = second_number
    27
    28    # streamlit.session_state['number'] = str(
    29    #     float(streamlit.session_state['first_number'])
    30    #   + float(streamlit.session_state['second_number'])
    31    # )
    32    operation = streamlit.session_state['operation']
    33    result = arithmetic[operation](
    34        float(streamlit.session_state['first_number']),
    35        float(streamlit.session_state['second_number'])
    36    )
    37    streamlit.session_state['number'] = str(result)
    

    the test passes

  • I remove the commented lines

    20def calculate():
    21    arithmetic = {
    22        r'\+': calculator.add,
    23        r'\-': calculator.subtract,
    24    }
    25    second_number = streamlit.session_state['number']
    26    streamlit.session_state['second_number'] = second_number
    27
    28    operation = streamlit.session_state['operation']
    29    result = arithmetic[operation](
    30        float(streamlit.session_state['first_number']),
    31        float(streamlit.session_state['second_number'])
    32    )
    33    streamlit.session_state['number'] = str(result)
    34
    35
    36def plus_minus():
    

  • I add a dictionary for operations to test_streamlit_calculator_operations in test_streamlit_calculator.py

    158    def test_streamlit_calculator_operations(self):
    159        arithmetic = {
    160            r'\+': src.calculator.add,
    161            r'\-': src.calculator.subtract,
    162            '/': src.calculator.divide,
    163            'X': src.calculator.multiply,
    164        }
    165        first_number = '1'
    166        second_number = '2'
    

    the terminal_ shows NameError

    NameError: name 'src' is not defined
    
  • I add an `import statement`_ at the top of the file_

    1import random
    2import src.calculator
    3import streamlit.testing.v1
    4import tests.test_calculator
    5import unittest
    6
    7
    8class TestStreamlitCalculator(unittest.TestCase):
    

    the test is green again

  • I use the dictionary in a new assertion

    
    
  • I use the a_random_number function from test_calculator.py to add randomness to test_streamlit_calculator_operations

    181    def test_streamlit_calculator_operations(self):
    182        # first_number = '1'
    183        first_number = tests.test_calculator.a_random_number()
    184        second_number = '2'
    185
    186        self.tester.button(first_number).click().run()
    

    the terminal_ shows KeyError

    KeyError: ABC.DEFGHIJKLMNOP
    
  • I have to change the number to button presses. I change the number to a string_

    181    def test_streamlit_calculator_operations(self):
    182        # first_number = '1'
    183        first_number = tests.test_calculator.a_random_number()
    184        first_number = str(first_number)
    185        second_number = '2'
    

    the terminal_ shows KeyError

    KeyError: ‘QRS.TUVWXYZABCDEF’

  • I add a for loop for the button presses

    181    def test_streamlit_calculator_operations(self):
    182        # first_number = '1'
    183        first_number = tests.test_calculator.a_random_number()
    184        first_number = str(first_number)
    185        second_number = '2'
    186
    187        for character in first_number:
    188            self.tester.button(character).click().run()
    189        # self.tester.button(first_number).click().run()
    190        self.tester.button('+').click().run()
    

    I use ctrl+s on the keyboard to run the test a few times, sometimes it passes and sometimes it shows AssertionError

    AssertionError: 'GH.IJKLMNOPQRSTUVW' != '-GH.IJKLMNOPQRSTUVW'
    

    there is no button for - it is +/-

  • I add a condition to the for loop

    187        for character in first_number:
    188            if character == '-':
    189                character = '+/-'
    190            self.tester.button(character).click().run()
    191        self.tester.button('+').click().run()
    
  • I do the same thing with the second number

    181    def test_streamlit_calculator_operations(self):
    182        # first_number = '1'
    183        first_number = tests.test_calculator.a_random_number()
    184        first_number = str(first_number)
    185
    186        # second_number = '2'
    187        second_number = tests.test_calculator.a_random_number()
    188        second_number = str(second_number)
    189
    190        for character in first_number:
    

    the terminal_ shows KeyError

    
    


test_error_messages_in_streamlit

RED: make it fail

I add

20    def test_error_messages_in_streamlit(self):
21        self.assertEqual(
22            src.calculator.divide(10, 0),
23            'brmph?! I cannot divide by 0. Try again...'
24        )

the test already passes (thanks to chapters 3–4).


how to run the app

In the terminal I type:

uv run streamlit run src/streamlit_app.py

A browser window opens automatically with my beautiful calculator!


close the project


review

I now have three different versions of the same calculator:

  • Pure Python (chapters 1–8)

  • Flask website (chapter 9)

  • Streamlit web app (chapter 10) — the fastest and most beautiful version

The core calculator code never changed. All my tests still protect it. This is the real power of Test-Driven Development.

code from the chapter

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


what is next?

You have completed an amazing journey from pure functions to real web applications!

You now know how to:

  • Build programs with Test-Driven Development

  • Turn them into Flask websites

  • Turn them into beautiful Streamlit apps


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