how to make a calculator 10: part 4
open the project
I `change directory`_ to the
calculatorfolder_cd calculatorI make a new test file_ for the Streamlit_ website
touch tests/test_streamlit_calculator.pyI add streamlit_ to the
requirements.txtfile_echo "streamlit" >> requirements.txtStreamlit_ 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.txtthe terminal shows it installed the `Python packages`_
I use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe 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'(thenumbersvariable)self.tester.button(number).click().run()presses the random numberself.assertEqual(self.tester.session_state['number'], number)checks that the value of thenumberkey in the `session state object`_ is the same as the number that was pressedself.tester.button('C').click().run()presses theCbuttonself.assertEqual(self.tester.session_state['number'], '0')checks that the thenumberkey in the `session state object`_ is set back to0after theCbutton is pressed
GREEN: make it pass
I add a function to
streamlit_calculator.py4def 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_clickandargsparameters to theCbutton in theadd_buttons_to_column_2function65def 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
Cit clears the numbers I typeI add an assertion for the
ACbutton in test_streamlit_calculator_reset129 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 seenthe terminal_ shows AssertionError
AssertionError: 'D' != '0'I add the
on_clickandargsparameters to theACbutton in theadd_buttons_to_column_3function88def 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
ACit 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
KeyError: '+'
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()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, instreamlit_calculator.py8def 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_clickandargsparameters to the+button in theadd_buttons_to_column_4function116def 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
displayto theargslist126 column_4.button( 127 r'\+', key=r'\+', width='stretch', on_click=on_click, 128 args=[first_number, display], type='primary', 129 )I add
displayas a positional argument of theadd_buttons_to_column_4function116def add_buttons_to_column_4(column_4, display):the terminal_ shows AssertionError for more tests
I add
displayin the call to theadd_buttons_to_column_4function from theadd_buttonsfunction135def 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.0I add an assertion for the second number after the
=button is pressed, intest_streamlit_calculator.py158 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 )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.py12def 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_clickandargsparameters to the=button in theadd_buttons_to_column_4function121def 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.0I add a calculation to the
calculatefunction18def 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.0I 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
the result is correct but the number looks different from the others
when I try another number, the browser shows TypeError
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
calculatefunctionthe terminal_ shows AssertionError
AssertionError: '3.0' != 3.0I change the expectation of the assertion in test_streamlit_calculator_operations in
test_streamlit_calculator.py176 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
I like it, though I do not need the
.0after the3I 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 seenthe 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 )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_numberfunction instreamlit_calculator.py12def 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()I add the value for
operationto theargslist for ther'\+'button in theadd_buttons_to_column_4function137 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
operationkey after the-button is pressed, intest_streamlit_calculator.py158 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 seenthe terminal_ shows AssertionError
AssertionError: '\\+' != '\\-'I use the operation in a dictionary in the
calculatefunction instreamlit_calculator.py19def 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 )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_clickandargsparameters to the-button in theadd_buttons_to_column_4function138 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
operationfor subtraction to thecalculatefunction20def 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.py158 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'NameError: name 'src' is not definedI 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_numberfunction fromtest_calculator.pyto add randomness to test_streamlit_calculator_operations181 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()KeyError: ABC.DEFGHIJKLMNOPI 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'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:
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
I close all files
I click in the terminal_, then use q to leave the tests
I `change directory`_ to the parent
cd ..
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
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