how to make a calculator 11
I want to use a Large Language Model to make a calculator with Python
I ask Gemini
I go to https://gemini.google.com then type this in the chat
use python to make a web based calculator that does arithmeticGemini returns this code for the backend
app.pyfrom flask import Flask, render_template, request app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) def calculator(): result = None if request.method == 'POST': try: # Fetch numbers and operation from the form num1 = float(request.form.get('num1')) num2 = float(request.form.get('num2')) operation = request.form.get('operation') if operation == 'add': result = num1 + num2 elif operation == 'subtract': result = num1 - num2 elif operation == 'multiply': result = num1 * num2 elif operation == 'divide': result = num1 / num2 if num2 != 0 else "Error: Division by zero" except (ValueError, TypeError): result = "Error: Invalid Input" return render_template('index.html', result=result) if __name__ == '__main__': app.run(debug=True)Gemini returns this code for the frontend
templates/index.html<!DOCTYPE html> <html lang="en"> <head> <title>Python Calculator</title> <style> body { font-family: sans-serif; display: flex; justify-content: center; padding-top: 50px; } .calc-card { border: 1px solid #ccc; padding: 20px; border-radius: 8px; box-shadow: 2px 2px 10px #eee; } input, select, button { margin: 10px 0; display: block; width: 100%; padding: 8px; } </style> </head> <body> <div class="calc-card"> <h2>Arithmetic Calculator</h2> <form method="POST"> <input type="number" step="any" name="num1" placeholder="First Number" required> <select name="operation"> <option value="add">+</option> <option value="subtract">-</option> <option value="multiply">×</option> <option value="divide">÷</option> </select> <input type="number" step="any" name="num2" placeholder="Second Number" required> <button type="submit">Calculate</button> </form> {% if result is not none %} <h3>Result: {{ result }}</h3> {% endif %} </div> </body> </html>it also included these instructions
1. Install Flask: Open your terminal and run pip install flask. 2. Organize Files: app.py templates/ index.html 3. Launch: Run python app.py. 4. View: Open your browser and go to http://127.0.0.1:5000. Why this works - Request Handling: The POST method allows the browser to send data to the server securely. - Jinja2 Templating: The {{ result }} syntax in the HTML is a placeholder that Flask fills in dynamically once the calculation is done. - Error Trapping: Using a try/except block prevents the whole web server from crashing if a user types in something weird.
I ask Grok
I go to https://grok.com then type this in the chat
use python to make a web based calculator that does arithmeticGrok returns this code for a file named
calculator.py# file: calculator.py from flask import Flask, request, render_template_string app = Flask(__name__) # HTML + very basic CSS + JavaScript HTML = """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Simple Arithmetic Calculator</title> <style> body { font-family: Arial, sans-serif; max-width: 420px; margin: 40px auto; text-align: center; background: #f8f9fa; } .calculator { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } input[type="text"] { width: 100%; font-size: 2.4rem; text-align: right; padding: 12px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 8px; background: #f9f9f9; } .buttons { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; } button { font-size: 1.5rem; padding: 18px; border: none; border-radius: 10px; cursor: pointer; background: #e9ecef; transition: all 0.12s; } button:hover { background: #dee2e6; } button.operator { background: #fd7e14; color: white; } button.operator:hover { background: #e06b00; } button.equals { background: #28a745; color: white; grid-column: span 2; } button.equals:hover { background: #218838; } button.clear { background: #dc3545; color: white; } button.clear:hover { background: #c82333; } </style> </head> <body> <div class="calculator"> <h2>Calculator</h2> <form method="POST"> <input type="text" id="display" name="expression" readonly value="{{ result if result else '' }}" /> <div class="buttons"> <button type="button" onclick="append('7')">7</button> <button type="button" onclick="append('8')">8</button> <button type="button" onclick="append('9')">9</button> <button type="button" class="operator" onclick="append('/')">/</button> <button type="button" onclick="append('4')">4</button> <button type="button" onclick="append('5')">5</button> <button type="button" onclick="append('6')">6</button> <button type="button" class="operator" onclick="append('*')">×</button> <button type="button" onclick="append('1')">1</button> <button type="button" onclick="append('2')">2</button> <button type="button" onclick="append('3')">3</button> <button type="button" class="operator" onclick="append('-')">-</button> <button type="button" onclick="append('0')">0</button> <button type="button" onclick="append('.')">.</button> <button type="button" class="clear" onclick="clearDisplay()">C</button> <button type="button" class="operator" onclick="append('+')">+</button> <button type="submit" class="equals">=</button> </div> </form> </div> <script> const display = document.getElementById('display'); function append(value) { display.value += value; } function clearDisplay() { display.value = ''; } </script> </body> </html> """ @app.route("/", methods=["GET", "POST"]) def calculator(): result = None if request.method == "POST": expr = request.form.get("expression", "") try: # Replace × with * for Python expr = expr.replace("×", "*") # Very basic safety — only allow numbers and operators allowed = set("0123456789.+-*/() ") if all(c in allowed for c in expr): result = eval(expr) # ← danger zone in real apps! else: result = "Invalid characters" except Exception as e: result = "Error" return render_template_string(HTML, result=result) if __name__ == "__main__": print("Calculator running at: http://127.0.0.1:5000") app.run(debug=True)Grok included these
How to run it 1. Make sure you have Flask installed: `pip install flask` 2. Save the code above as ``calculator.py`` 3. Run it `python calculator.py` 4. Open your browser and go to: http://127.0.0.1:5000 You should see a nice-looking calculator
I ask ChatGPT
I go to https://chatgpt.com then type this in the chat
use python to make a web based calculator that does arithmeticChatGPT returns this code for a file named
calculator.pyChatGPT also includes instruction on how to run the code
it also included these instructions
I ask Claude
I go to https://grok.com then type this in the chat
use python to make a web based calculator that does arithmeticGrok returns this code for the backend
app.pyGrok returns this code for the frontend
templates/index.htmlit also included these instructions
I ask ChatGPT
I go to https://chatgpt.com then type this in the chat
use python to make a web based calculator that does arithmeticGrok returns this code for the backend
app.pyGrok returns this code for the frontend
templates/index.htmlit also included these instructions
preview
These are the tests I have by the end of the chapter
1import src.website
2import tests.test_calculator
3import unittest
4
5
6class TestCalculatorWebsite(unittest.TestCase):
7
8 def setUp(self):
9 self.client = src.website.app.test_client()
10 self.x = tests.test_calculator.a_random_number()
11
12 def test_home_page(self):
13 response = self.client.get('/')
14 self.assertEqual(response.status_code, 200)
15 self.assertIn(
16 b'<h1>Calculator</h1>',
17 response.data
18 )
19
20 def test_calculations(self):
21 y = tests.test_calculator.a_random_number()
22
23 operations = {
24 'add': '+',
25 'subtract': '-',
26 'divide': '/',
27 'multiply': '*',
28 }
29
30 for operation in operations:
31 with self.subTest(operation=operation):
32 response = self.client.post(
33 '/calculate',
34 data={
35 'first_input': self.x,
36 'second_input': y,
37 'operation': operation,
38 }
39 )
40 self.assertEqual(response.status_code, 200)
41
42 function = src.calculator.__getattribute__(
43 operation
44 )
45 result = function(self.x, y)
46 self.assertEqual(
47 response.data.decode(),
48 (
49 f'<h2>{self.x} {operations[operation]} {y} '
50 f'= {result}</h2>'
51 )
52 )
53
54 def test_website_handling_zero_division_error(self):
55 response = self.client.post(
56 '/calculate',
57 data={
58 'first_input': self.x,
59 'second_input': 0,
60 'operation': 'divide',
61 }
62 )
63 self.assertEqual(
64 response.data.decode(),
65 (
66 f'<h2>{self.x} / 0.0 = '
67 'brmph?! I cannot divide by 0. Try again...</h2>'
68 )
69 )
70
71
72# Exceptions seen
73# NameError
74# ModuleNotFoundError
75# AttributeError
76# AssertionError
77# jinja2.exceptions.TemplateNotFound
78# SyntaxError
I change directory to the
calculatorfoldercd calculatorthe terminal shows I am in the
calculatorfolder.../pumping_python/calculatorI make a new test file for the website
touch tests/test_calculator_website.pyI add Flask_ to the
requirements.txtfileecho "flask" >> requirements.txtFlask_ 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 5 items tests/test_calculator.py ..... [100%] ======================== 5 passed in X.YZs =========================
test_home_page
RED: make it fail
I open
test_calculator_website.pyfrom thetestsfolder in the editorI add a test to
test_calculator_website.py1import unittest 2 3 4class TestCalculatorWebsite(unittest.TestCase): 5 6 def test_home_page(self): 7 client = src.website.app.test_client()NameError: name 'src' is not definedI add NameError to the list of Exceptions seen
4class TestCalculatorWebsite(unittest.TestCase): 5 6 def test_home_page(self): 7 client = src.website.app.test_client() 8 9 10# Exceptions seen 11# NameError
GREEN: make it pass
I add an import statement for the
websitemodule1import src.website 2import unittestthe terminal shows ModuleNotFoundError
ModuleNotFoundError: No module named 'src.website'I add ModuleNotFoundError to the list of Exceptions seen
11# Exceptions seen 12# NameError 13# ModuleNotFoundErrorthen I add
website.pyto thesrcfolder and the terminal shows AttributeErrorAttributeError: module 'src.website' has no attribute 'app'I add AttributeError to the list of Exceptions seen
11# Exceptions seen 12# NameError 13# ModuleNotFoundError 14# AttributeErrorI make the Flask_ app in
website.py1import flask 2 3 4app = flask.Flask(__name__)the test passes
REFACTOR: make it better
I add an assertion to test_home_page in
test_calculator_website.py7 def test_home_page(self): 8 client = src.website.app.test_client() 9 response = client.get('/') 10 self.assertEqual(response.status_code, 'BOOM!!!')the terminal shows AssertionError
AssertionError: 404 != 'BOOM!!!'I add AssertionError to the list of Exceptions seen
13# Exceptions seen 14# NameError 15# ModuleNotFoundError 16# AttributeError 17# AssertionErrorI change the expectation in the assertion
10 self.assertEqual(response.status_code, 404)the test passes.
client = src.website.app.test_client()points the nameclientto the Flask_ app inwebsite.pyresponse = client.get('/')points the nameresponseto the result of the call to thegetmethod of the client objectthe
getmethod calls the `GET request method`_ which is an `HTTP request method`_ to get information from a serverclient.get('/')returns a response object'/'is short for root or home in this case, the homepage of the website I am making also known asindex.htmlresponse.status_codegets thestatus_codeattribute or the response objectthe above can also be written as
src.website.app.test_client().get('/').status_code404is `HTTP status code`_, it is short for `404 Not Found`_ which means the page cannot be foundI want a
200`HTTP status code`_, it is short for `200 OK`_ and means the request was successful
I change the expectation in the assertion
7 def test_home_page(self): 8 client = src.website.app.test_client() 9 response = client.get('/') 10 self.assertEqual(response.status_code, 200)the terminal shows AssertionError
AssertionError: 404 != 200I add a function for the homepage in
website.py1import flask 2 3 4app = flask.Flask(__name__) 5 6 7@app.route('/') 8def home(): 9 return flask.render_template('index.html')the terminal shows AssertionError
AssertionError: 500 != 200it also shows `jinja2.exceptions.TemplateNotFound`_
jinja2.exceptions.TemplateNotFound: index.htmlI have to make a template file for
index.html@app.routeis a decorator function that routes the pages of the website to the function it wraps'/'is short for root or home in this case, the homepage of the website I am making also known asindex.html
I add `jinja2.exceptions.TemplateNotFound`_ to the list of Exceptions seen
# Exceptions seen # NameError # ModuleNotFoundError # AttributeError # AssertionError # jinja2.exceptions.TemplateNotFoundI add a new folder to the
srcfolder namedtemplates, the terminal still shows the same ExceptionI add a new file in the
templatesfolder namedindex.html, the terminal still shows the same ExceptionI go back to
test_calculator_website.pythen use ctrl+s on the keyboard to save the file which makes pytest-watcher run the tests again, and the test passes
how to view the website
I want to see what I just made. I open a new terminal, then use uv to run the Flask_ server
uv run flask --app src/website run --debugthe terminal shows
* Serving Flask app 'src/website' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit * Restarting with watchdog (inotify) * Debugger is active! * Debugger PIN: ABC-DEF-GHIit might also show a dialog box like this, and I click on
Open in Browser
or I use ctrl/option on the keyboard and click on
http://127.0.0.1:5000with the mouse to open the browser and it shows an empty page. Success!I add another assertion for text from the website to test_home_page in
test_calculator_website.py7 def test_home_page(self): 8 client = src.website.app.test_client() 9 response = client.get('/') 10 self.assertEqual(response.status_code, 200) 11 self.assertEqual( 12 response.data, 13 '<h1>Calculator</h1>' 14 )the terminal shows AssertionError
AssertionError: b'' != '<h1>Calculator</h1>'there is nothing in
src/templates/index.htmlb''is the empty `bytes object`_`bytes objects`_ are like strings , they have a
bbefore the quotes, for exampleb'single quote bytes' b'''triple single quote bytes''' b"double quote bytes" b"""triple double quote bytes"""
I open
index.htmlfrom thetemplatesfolder in the editor, then add some HTML1<h1>Calculator</h1>the terminal still shows AssertionError because pytest-watcher only checks my Python files, it does not run the tests when I change other files
<></>are called tags, they are enclosures<h1>HEADING</h1>tells the computer to makeHEADINGa heading
I go back to
test_calculator_website.pyand use ctrl+s on the keyboard to run the test, the terminal shows AssertionErrorAssertionError: b'<h1>Calculator</h1>' != '<h1>Calculator</h1>'I make the expectation a `bytes object`_
11 self.assertEqual( 12 response.data, 13 b'<h1>Calculator</h1>' 14 )the test passes
I go back to the browser and click
refresh, it showsCalculator
Yes! I know how to make a website with Python!!
test_calculations
RED: make it fail
I add a new test for doing calculations with the website
7 def test_home_page(self):
8 client = src.website.app.test_client()
9 response = client.get('/')
10 self.assertEqual(response.status_code, 200)
11 self.assertEqual(
12 response.data.decode(),
13 '<h1>Calculator</h1>'
14 )
15
16 def test_calculation(self):
17 client = src.website.app.test_client()
18 response = client.post(
19 '/calculate',
20 data={
21 'first_input': 0,
22 'second_input': 1,
23 }
24 )
25 self.assertEqual(response.status_code, 200)
26
27
28 # Exceptions seen
the terminal shows AssertionError
AssertionError: 404 != 200
this is like AttributeError, the address for calculate does not exist yet.
client.postcalls thepostmethodwhich calls the `POST request method`_, an `HTTP request method`_ that sends information to a server. I am using it to test sending numbers to the website to do a calculation
the
postmethod is called with 2 inputs in this case'/calculate'- the path to the function inwebsite.pydata- a dictionary with the inputs I want the function to process
GREEN: make it pass
I add a new function to
website.py7 @app.route('/') 8 def home(): 9 return flask.render_template('index.html') 10 11 12 @app.route('/calculate', methods=['POST']) 13 def calculate(): 14 first_input = flask.request.form.get('first_input') 15 second_input = flask.request.form.get('second_input') 16 17 result = calculator.add(first_input, second_input) 18 return ( 19 f'<h2>{first_input} + {second_input} ' 20 f'= {result}</h2>' 21 )@app.routeis a decorator function that routes the pages of the website to the function it wraps'/calculate'is the route I want to point to thecalculatefunction['POST']is a list of `HTTP request methods`_ that I can send, in this case I am using the `POST request method`_ to send information to the serverflask.request.form.get(NAME)uses the get method to get the value of theNAMEkey from the dictionary when the user makes a request<h2>SMALLER HEADING</h2>tells the computer to makeSMALLER HEADINGa heading that is smaller thanh1headings
NameError: name 'calculator' is not defined. Did you mean: 'calculate'?I add an import statement for the
calculatormodule at the top of the file1import calculator 2import flaskthe terminal shows ModuleNotFoundError
ModuleNotFoundError: No module named 'calculatorbecause the test cannot find the
calculator.pymoduleI change the import statement
1import src.calculator 2import flaskthe test passes and the terminal for the website shows ModuleNotFoundError
ModuleNotFoundError: No module named 'src'this is a problem
website.pyis in thesrcfolder withcalculator.pysoimport src.calculatorwill not work for the websitetest_calculator_website.pyis in thetestsfolder soimport calculatorwill not work for the tests
I need a way that allows both the tests and website to work
I change the import statement back
1import calculator 2import flaskthe terminal shows ModuleNotFoundError
ModuleNotFoundError: No module named 'calculator'I add an import statement for the sys module, it is part of The Python Standard Library
1import sys 2 3import calculator 4import flaskI can use sys to get variables that the computer uses without going to the terminal
I add another import statement
1import pathlib 2import sys 3 4import calculator 5import flaskthe pathlib module is also part of The Python Standard Library
I can use it to get paths(addresses) for files and folders on the computer without going to the terminal
I use pathlib to add the path for
calculator.pyto the `system path`_ which is a list of strings where Python looks for modules when I write an import statement1import pathlib 2import sys 3sys.path.insert( 4 0, str(pathlib.Path(__file__).resolve().parent) 5) 6 7import calculator 8import flaskthe test passes and the terminal for the browser shows no errors
the path to
calculator.pyis now correct forwebsite.pyandtest_calculator.pysys.path.insertuses the insert method of lists to place thesrcfolder as the first item in the list for Python to look for modulespathlib.Path(__file__).resolve().parentreturns the parent of the current file -srcin this case__file__is a variable with the name of the filewebsite.pyin this casethe route to
/calculatenow exists
REFACTOR: make it better
I add an assertion to make sure I get the right result back in
test_calculator_website.py13 def test_calculations(self): 14 client = src.website.app.test_client() 15 response = client.post( 16 '/calculate', 17 data={ 18 'first_input': 0, 19 'second_input': 1, 20 } 21 ) 22 self.assertEqual(response.status_code, 200) 23 self.assertEqual(response.data, 'BOOM!!!')the terminal shows AssertionError
AssertionError: b'<h2>0 + 1 = brmph?! Numbers only. Try again...</h2>' != 'BOOM!!!'Ah! The calculator only works with numbers. The numbers I sent as values in the test
0and1are getting converted to something the calculator does not likeI change
first_inputto a float inwebsite.py19@app.route('/calculate', methods=['POST']) 20def calculate(): 21 first_input = flask.request.form.get('first_input') 22 first_input = float(first_input) 23 second_input = flask.request.form.get('second_input')TypeError: unsupported operand type(s) for +: 'float' and 'str'I change
second_inputto a float19@app.route('/calculate', methods=['POST']) 20def calculate(): 21 first_input = flask.request.form.get('first_input') 22 first_input = float(first_input) 23 second_input = flask.request.form.get('second_input') 24 second_input = float(second_input) 25 26 result = calculator.add(first_input, second_input) 27 return ( 28 f'<h2>{first_input} + {second_input} ' 29 f'= {result}</h2>' 30 )the terminal shows AssertionError
AssertionError: b'<h2>0.0 + 1.0 = 1.0</h2>' != 'BOOM!!!'I copy the result from the terminal and paste it in
test_calculator_website.py16 def test_calculations(self): 17 client = src.website.app.test_client() 18 response = client.post( 19 '/calculate', 20 data={ 21 'first_input': 0, 22 'second_input': 1, 23 } 24 ) 25 self.assertEqual(response.status_code, 200) 26 self.assertEqual( 27 response.data, 28 b'<h2>0.0 + 1.0 = 1.0</h2>' 29 )the test passes
I want to use random numbers in the test. I add an import statement for the
test_calculatormodule1import src.website 2import tests.test_calculator 3import unittestI point a variable to the result of a call to the
a_random_numberfunction oftest_calculator17 def test_calculations(self): 18 x = tests.test_calculator.a_random_number() 19 client = src.website.app.test_client()I use the new variable as the first input
20 response = client.post( 21 '/calculate', 22 data={ 23 # 'first_input': 0, 24 'first_input': x, 25 'second_input': 1, 26 } 27 )the terminal shows AssertionError
AssertionError: b'<h2>ABC.DEFGHIJKLMNOP + 1.0 = ABQ.DEFGHIJKLMNOP</h2>' != b'<h2>0.0 + 1.0 = 1.0</h2>'good, the input gets to the website
I try an f-string so I can use the variable in the assertion
29 self.assertEqual( 30 response.data, 31 fb'<h2>0.0 + 1.0 = 1.0</h2>' 32 )the terminal shows SyntaxError
SyntaxError: invalid syntaxI cannot make a `bytes object`_ an f-string
I add SyntaxError to the list of Exceptions seen
35# Exceptions seen 36# NameError 37# ModuleNotFoundError 38# AttributeError 39# AssertionError 40# jinja2.exceptions.TemplateNotFound 41# SyntaxError
how to change a bytes object to a string
I change the `bytes object`_ to a string with `bytes.decode`_
29 self.assertEqual( 30 response.data.decode(), 31 f'<h2>0.0 + 1.0 = 1.0</h2>' 32 )the terminal shows AssertionError
AssertionError: '<h2>RS.TUVWXYZABCDEFG + 1.0 = RH.TUVWXYZABCDEFG</h2>' != '<h2>0.0 + 1.0 = 1.0</h2>'response.data.decode()returns the result of calling the `decode method`_ of thedataattribute of theresponseobjectthe `decode method`_ is part of the bytes data type, it converts bytes to strings
I add the variable to the expectation
29 self.assertEqual( 30 response.data.decode(), 31 f'<h2>{x} + 1.0 = {x+1}</h2>' 32 )the test passes
I add another variable
17 def test_calculations(self): 18 x = tests.test_calculator.a_random_number() 19 y = tests.test_calculator.a_random_number() 20 client = src.website.app.test_client()I use the variable for the second input
21 response = client.post( 22 '/calculate', 23 data={ 24 # 'first_input': 0, 25 'first_input': x, 26 # 'second_input': 1, 27 'second_input': y, 28 } 29 )the terminal shows AssertionError
AssertionError: '<h2>HIJ.KLMNOPQRSTUVWX + YZA.BCDEFGHIJKLMNOP = QRS.TUVWXYZABCDEFG</h2>' != '<h2>-HIJ.KLMNOPQRSTUVWX + 1.0 = HIY.KLMNOPQRSTUVWX</h2>'I add
yto the f-string in the expectation31 self.assertEqual( 32 response.data.decode(), 33 f'<h2>{x} + {y} = {x+y}</h2>' 34 )the test passes
I remove the comments
17 def test_calculations(self): 18 x = tests.test_calculator.a_random_number() 19 y = tests.test_calculator.a_random_number() 20 client = src.website.app.test_client() 21 response = client.post( 22 '/calculate', 23 data={ 24 'first_input': x, 25 'second_input': y, 26 } 27 ) 28 self.assertEqual(response.status_code, 200) 29 self.assertEqual( 30 response.data.decode(), 31 f'<h2>{x} + {y} = {x+y}</h2>' 32 )the test is still green
I want to test the other operations. I add a dictionary for the operations
17 def test_calculations(self): 18 x = tests.test_calculator.a_random_number() 19 y = tests.test_calculator.a_random_number() 20 21 operations = { 22 'add': '+', 23 'subtract': '-', 24 'divide': '/', 25 'multiply': '*', 26 } 27 28 client = src.website.app.test_client()I add a for loop to the test
28 client = src.website.app.test_client() 29 30 for operation in operations: 31 with self.subTest(operation=operation): 32 response = client.post( 33 '/calculate', 34 data={ 35 'first_input': x, 36 'second_input': y, 37 } 38 ) 39 self.assertEqual(response.status_code, 200) 40 self.assertEqual( 41 response.data.decode(), 42 f'<h2>{x} + {y} = {x+y}</h2>' 43 ) 44 45 46# Exceptions seenI add a key to the
datadictionary32 response = client.post( 33 '/calculate', 34 data={ 35 'first_input': x, 36 'second_input': y, 37 'operation': operation, 38 } 39 )I add a variable for the calculator function
40 self.assertEqual(response.status_code, 200) 41 42 function = src.calculator.__getattribute__( 43 operation 44 ) 45 self.assertEqual( 46 response.data.decode(), 47 f'<h2>{x} + {y} = {x+y}</h2>' 48 )I add another variable for the expected result
42 function = src.calculator.__getattribute__( 43 operation 44 ) 45 result = function(x, y) 46 self.assertEqual( 47 response.data.decode(), 48 f'<h2>{x} + {y} = {x+y}</h2>' 49 )I change the expectation in the assertion to use the new variables
46 self.assertEqual( 47 response.data.decode(), 48 # f'<h2>{x} + {y} = {x+y}</h2>' 49 ( 50 f'<h2>{x} {operations[operation]} {y} ' 51 f'= {result}</h2>' 52 ) 53 )the terminal shows AssertionError
AssertionError: '<h2>YZA.BCDEFGHIJKLMNO + PQR.STUVWXYZABCDEFG = HIJ.KLMNOPQRSTUVWXY</h2>' != '<h2>YZA.BCDEFGHIJKLMNO * PQR.STUVWXYZABCDEFG = ZABCD.EFGHIJKLMNO</h2>'the subtest for addition passes
the operation symbols for the other 3 functions and their results changed
I add a variable for value the test passes for the operation
19@app.route('/calculate', methods=['POST']) 20def calculate(): 21 first_input = flask.request.form.get('first_input') 22 first_input = float(first_input) 23 second_input = flask.request.form.get('second_input') 24 second_input = float(second_input) 25 operation = flask.request.form.get('operation') 26 27 result = calculator.add(first_input, second_input)the test still shows AssertionError for the 3 sub tests
I add a dictionary for the operations
25 operation = flask.request.form.get('operation') 26 27 operations = { 28 'add': '+', 29 'subtract': '-', 30 'divide': '/', 31 'multiply': '*', 32 } 33 34 result = calculator.add(first_input, second_input)I use the
__getattribute__method for the result34 # result = calculator.add(first_input, second_input) 35 result = calculator.__getattribute__(operation)( 36 first_input, second_input 37 ) 38 return ( 39 f'<h2>{first_input} + {second_input} ' 40 f'= {result}</h2>' 41 )I change the return statement
38 return ( 39 # f'<h2>{first_input} + {second_input} ' 40 f'<h2>{first_input} {operations[operation]} {second_input} ' 41 f'= {result}</h2>' 42 )the test passes
I remove the commented lines
19@app.route('/calculate', methods=['POST']) 20def calculate(): 21 first_input = flask.request.form.get('first_input') 22 first_input = float(first_input) 23 second_input = flask.request.form.get('second_input') 24 second_input = float(second_input) 25 operation = flask.request.form.get('operation') 26 27 operations = { 28 'add': '+', 29 'subtract': '-', 30 'divide': '/', 31 'multiply': '*', 32 } 33 34 result = calculator.__getattribute__(operation)( 35 first_input, second_input 36 ) 37 return ( 38 f'<h2>{first_input} {operations[operation]} {second_input} ' 39 f'= {result}</h2>' 40 )the test is still green
I remove the comments from test_calculations in
test_calculator_website.py17 def test_calculations(self): 18 x = tests.test_calculator.a_random_number() 19 y = tests.test_calculator.a_random_number() 20 21 operations = { 22 'add': '+', 23 'subtract': '-', 24 'divide': '/', 25 'multiply': '*', 26 } 27 28 client = src.website.app.test_client() 29 30 for operation in operations: 31 with self.subTest(operation=operation): 32 response = client.post( 33 '/calculate', 34 data={ 35 'first_input': x, 36 'second_input': y, 37 'operation': operation, 38 } 39 ) 40 self.assertEqual(response.status_code, 200) 41 42 function = src.calculator.__getattribute__( 43 operation 44 ) 45 result = function(x, y) 46 self.assertEqual( 47 response.data.decode(), 48 ( 49 f'<h2>{x} {operations[operation]} {y} ' 50 f'= {result}</h2>' 51 ) 52 ) 53 54 55# Exceptions seen
how to make a form
`HTML Forms`_ are the most popular thing we see with websites, we use them when we sign in or put any information on a website. I can add a form to the website for users to do calculations since the route now exists.
RED: make it fail
I add more HTML to
index.htmlin thetemplatesfolder in thesrcfolder1 <h1>Calculator</h1> 2 <form method="post" action="/calculate"> 3 </form>method="post"means the form will use the `POST request method`_action="/calculate"means the form will send the data to thecalculatefunction inwebsite.py
I go to the website and click refresh, there is no change
I click on
test_calculator_website.pyand use ctrl+s on the keyboard to run the tests again, the terminal shows AssertionErrorAssertionError: b'<h1>Calculator</h1>\n<form method="post" action="/calculate">\n</form>' != b'<h1>Calculator</h1>'the change made test_home_page fail because there is now more HTML
GREEN: make it pass
I change the assertion to look for the title and not the entire page
8 def test_home_page(self):
9 client = src.website.app.test_client()
10 response = client.get('/')
11 self.assertEqual(response.status_code, 200)
12 self.assertIn(
13 b'<h1>Calculator</h1>',
14 response.data
15 )
16
17 def test_calculations(self):
the test passes because the `assertIn method`_ of the unittest.TestCase class checks if the thing on the left is in the object on the right
REFACTOR: make it better
I add more HTML to
index.htmlin thetemplatesfolder in thesrcfolder2<form method="post" action="/calculate"> 3 <input type="number" name="first_input" required> 4</form>I go to the browser and click refresh to see the change, the website has a place where I can put numbers or use the up and down arrows to change the numbers
I use ctrl+s on the keyboard in
test_calculator_website.pyto run the tests, still greenI add another input for the second number in
index.html2<form method="post" action="/calculate"> 3 <input type="number" name="first_input" required> 4 <input type="number" name="second_input" required> 5</form>I go to the browser and click refresh to see the change, the website now has 2 places for me to put numbers
I use ctrl+s on the keyboard in
test_calculator_website.pyto run the tests, greenI add options for the operation in
index.html2<form method="post" action="/calculate"> 3 <input type="number" name="first_input" required> 4 <input type="number" name="second_input" required> 5 <select name="operation"> 6 <option value="add">+</option> 7 </select> 8</form>I go to the browser and click refresh to see the change, the website now has a place for me to choose the operation, even though
+is the only option
I use ctrl+s on the keyboard in
website.pyto run the tests, they are still greenI want the operation to show up between the 2 numbers, I change the order in
index.html2<form method="post" action="/calculate"> 3 <input type="number" name="first_input" required> 4 <select name="operation"> 5 <option value="add">+</option> 6 </select> 7 <input type="number" name="second_input" required> 8</form>I go to the browser and click refresh to see the change, the operation is now between the 2 numbers
I use ctrl+s on the keyboard in
website.pyto run the tests. GreenI add the other operations to
index.html2<form method="post" action="/calculate"> 3 <input type="number" name="first_input" required> 4 <select name="operation"> 5 <option value="add">+</option> 6 <option value="subtract">-</option> 7 <option value="divide">/</option> 8 <option value="multiply">*</option> 9 </select> 10 <input type="number" name="second_input" required> 11</form>the browser shows all the options when I click refresh
I add a button to the form so the user can submit the numbers and operation for a calculation
1<h1>Calculator</h1> 2<form method="post" action="/calculate"> 3 <input type="number" name="first_input" required> 4 <select name="operation"> 5 <option value="add">+</option> 6 <option value="subtract">-</option> 7 <option value="divide">/</option> 8 <option value="multiply">*</option> 9 </select> 10 <input type="number" name="second_input" required> 11 <button type="submit">calculate</button> 12</form>I go to the browser and click refresh to see the change
I enter
10000as the first number and20000as the second number, with*as the operation
I click
calculateand the browser shows
ugly and it works
fix handling ZeroDivisionError in division
I want to make sure the division function returns a message when the second number is 0
RED: make it fail
I add a new test
17 def test_calculations(self):
18 x = tests.test_calculator.a_random_number()
19 y = tests.test_calculator.a_random_number()
20
21 operations = {
22 'add': '+',
23 'subtract': '-',
24 'divide': '/',
25 'multiply': '*',
26 }
27
28 client = src.website.app.test_client()
29
30 for operation in operations:
31 with self.subTest(operation=operation):
32 response = client.post(
33 '/calculate',
34 data={
35 'first_input': x,
36 'second_input': y,
37 'operation': operation,
38 }
39 )
40 self.assertEqual(response.status_code, 200)
41
42 function = src.calculator.__getattribute__(
43 operation
44 )
45 result = function(x, y)
46 self.assertEqual(
47 response.data.decode(),
48 (
49 f'<h2>{x} {operations[operation]} {y} '
50 f'= {result}</h2>'
51 )
52 )
53
54 def test_website_handling_zero_division_error(self):
55 x = tests.test_calculator.a_random_number()
56 client = src.website.app.test_client()
57
58 response = client.post(
59 '/calculate',
60 data={
61 'first_input': x,
62 'second_input': 0,
63 'operation': 'divide',
64 }
65 )
66 self.assertEqual(
67 response.data.decode(),
68 'brmph?! I cannot divide by 0. Try again...'
69 )
70
71
72# Exceptions seen
the terminal shows AssertionError in short test summary info
AssertionError: '<!doctype html>\n<html lang=en>\n<title>5[225 chars]p>\n' != 'brmph?! I cannot divide by 0.
and in the traceback it shows the AssertionError was caused by ZeroDivisionError
ZeroDivisionError: float division by zero
good
GREEN: make it pass
I open
calculator.pyfrom thesrcfolderI add an exception handler to the divide function in
calculator.py17@check_input 18def divide(first_input, second_input): 19 try: 20 return first_input / second_input 21 except ZeroDivisionError: 22 return 'brmph?! I cannot divide by 0. Try again...' 23 24 25@check_input 26def multiply(first_input, second_input):the terminal shows AssertionError
AssertionError: '<h2>PQ.RSTUVWXYZABCDEF / 0.0 = brmph?! I cannot divide by 0. Try again...</h2>' != 'brmph?! I cannot divide by 0. Try again...'okay. It returns the right error message
I change the expectation of the assertion in
test_calculator_website.py54 def test_website_handling_zero_division_error(self): 55 x = tests.test_calculator.a_random_number() 56 client = src.website.app.test_client() 57 58 response = client.post( 59 '/calculate', 60 data={ 61 'first_input': x, 62 'second_input': 0, 63 'operation': 'divide', 64 } 65 ) 66 self.assertEqual( 67 response.data.decode(), 68 ( 69 f'<h2>{x} / 0.0 = ' 70 'brmph?! I cannot divide by 0. Try again...</h2>' 71 ) 72 ) 73 74 75# Exceptions seenthe test passes
REFACTOR: make it better
I made the same client in each test. I add a class attribute for it in the setUp method
6class TestCalculatorWebsite(unittest.TestCase): 7 8 def setUp(self): 9 self.client = src.website.app.test_client() 10 11 def test_home_page(self):I use the new class attribute in test_home_page
11 def test_home_page(self): 12 # client = src.website.app.test_client() 13 client = self.client 14 response = client.get('/')the test is still green
I use it directly in the response
14 response = self.client.get('/')still green
I remove the
clientvariable and the commented line11 def test_home_page(self): 12 response = self.client.get('/') 13 self.assertEqual(response.status_code, 200) 14 self.assertIn( 15 b'<h1>Calculator</h1>', 16 response.data 17 ) 18 19 def test_calculations(self):green
I use the class attribute in test_calculations
32 for operation in operations: 33 with self.subTest(operation=operation): 34 # response = client.post( 35 response = self.client.post( 36 '/calculate', 37 data={ 38 'first_input': x, 39 'second_input': y, 40 'operation': operation, 41 } 42 )still green
I remove the commented line and the
clientvariable19 def test_calculations(self): 20 x = tests.test_calculator.a_random_number() 21 y = tests.test_calculator.a_random_number() 22 23 operations = { 24 'add': '+', 25 'subtract': '-', 26 'divide': '/', 27 'multiply': '*', 28 } 29 30 for operation in operations: 31 with self.subTest(operation=operation): 32 response = self.client.post( 33 '/calculate', 34 data={ 35 'first_input': x, 36 'second_input': y, 37 'operation': operation, 38 } 39 ) 40 self.assertEqual(response.status_code, 200) 41 42 function = src.calculator.__getattribute__( 43 operation 44 ) 45 result = function(x, y) 46 self.assertEqual( 47 response.data.decode(), 48 ( 49 f'<h2>{x} {operations[operation]} {y} ' 50 f'= {result}</h2>' 51 ) 52 ) 53 54 def test_website_handling_zero_division_error(self):the test is still green
I use the class attribute in test_website_handling_zero_division_error
54 def test_website_handling_zero_division_error(self): 55 x = tests.test_calculator.a_random_number() 56 client = src.website.app.test_client() 57 58 # response = client.post( 59 response = self.client.post( 60 '/calculate', 61 data={ 62 'first_input': x, 63 'second_input': 0, 64 'operation': 'divide', 65 } 66 )still green
I remove the commented line and
clientvariable54 def test_website_handling_zero_division_error(self): 55 x = tests.test_calculator.a_random_number() 56 response = self.client.post( 57 '/calculate', 58 data={ 59 'first_input': x, 60 'second_input': 0, 61 'operation': 'divide', 62 } 63 ) 64 self.assertEqual( 65 response.data.decode(), 66 ( 67 f'<h2>{x} / 0.0 = ' 68 'brmph?! I cannot divide by 0. Try again...</h2>' 69 ) 70 ) 71 72 73# Exceptions seengreen
I add class attributes for the random numbers to the setUp method
8 def setUp(self): 9 self.client = src.website.app.test_client() 10 self.x = tests.test_calculator.a_random_number() 11 12 def test_home_page(self):I use it in test_calculations
20 def test_calculations(self): 21 # x = tests.test_calculator.a_random_number() 22 x = self.x 23 y = tests.test_calculator.a_random_number()still green
I use the
Rename Symbolto changextoself.x20 def test_calculations(self): 21 # x = tests.test_calculator.a_random_number() 22 self.x = self.x 23 y = tests.test_calculator.a_random_number() 24 25 operations = { 26 'add': '+', 27 'subtract': '-', 28 'divide': '/', 29 'multiply': '*', 30 } 31 32 for operation in operations: 33 with self.subTest(operation=operation): 34 response = self.client.post( 35 '/calculate', 36 data={ 37 'first_input': self.x, 38 'second_input': y, 39 'operation': operation, 40 } 41 ) 42 self.assertEqual(response.status_code, 200) 43 44 function = src.calculator.__getattribute__( 45 operation 46 ) 47 result = function(self.x, y) 48 self.assertEqual( 49 response.data.decode(), 50 ( 51 f'<h2>{self.x} {operations[operation]} {y} ' 52 f'= {result}</h2>' 53 ) 54 )the test is still green
I remove the commented line and
self.x = self.x20 def test_calculations(self): 21 y = tests.test_calculator.a_random_number() 22 23 operations = { 24 'add': '+', 25 'subtract': '-', 26 'divide': '/', 27 'multiply': '*', 28 } 29 30 for operation in operations: 31 with self.subTest(operation=operation): 32 response = self.client.post( 33 '/calculate', 34 data={ 35 'first_input': self.x, 36 'second_input': y, 37 'operation': operation, 38 } 39 ) 40 self.assertEqual(response.status_code, 200) 41 42 function = src.calculator.__getattribute__( 43 operation 44 ) 45 result = function(self.x, y) 46 self.assertEqual( 47 response.data.decode(), 48 ( 49 f'<h2>{self.x} {operations[operation]} {y} ' 50 f'= {result}</h2>' 51 ) 52 ) 53 54 def test_website_handling_zero_division_error(self):I use the
Rename Symbolfeature to changextoself.xin test_website_handling_zero_division_error54 def test_website_handling_zero_division_error(self): 55 self.x = tests.test_calculator.a_random_number() 56 response = self.client.post( 57 '/calculate', 58 data={ 59 'first_input': self.x, 60 'second_input': 0, 61 'operation': 'divide', 62 } 63 ) 64 self.assertEqual( 65 response.data.decode(), 66 ( 67 f'<h2>{self.x} / 0.0 = ' 68 'brmph?! I cannot divide by 0. Try again...</h2>' 69 ) 70 )still green
I remove
self.x = tests.test_calculator.a_random_number()54 def test_website_handling_zero_division_error(self): 55 response = self.client.post( 56 '/calculate', 57 data={ 58 'first_input': self.x, 59 'second_input': 0, 60 'operation': 'divide', 61 } 62 ) 63 self.assertEqual( 64 response.data.decode(), 65 ( 66 f'<h2>{self.x} / 0.0 = ' 67 'brmph?! I cannot divide by 0. Try again...</h2>' 68 ) 69 ) 70 71 72# Exceptions seen
test_calculator_sends_message_when_inputs_are_not_numbers
time to fix the problem with the second input in test_calculator_sends_message_when_input_is_not_a_number
RED: make it fail
I open
test_calculator.pyfrom thetestsfolderI add another assertion where the second input is not a number and the first input is a number
151 self.assertEqual( 152 src.calculator.__getattribute__(operation)( 153 bad_input, a_random_number() 154 ), 155 'brmph?! Numbers only. Try again...' 156 ) 157 self.assertEqual( 158 src.calculator.__getattribute__(operation)( 159 a_random_number(), bad_input 160 ), 161 'brmph?! Numbers only. Try again...' 162 ) 163 164 def test_calculator_functions(self):TypeError: unsupported operand type(s) for /: 'float' and 'dictfor the 52 sub tests where the first input is a number and the second input is not a number
GREEN: make it pass
I add a condition to the if statement in the
check_inputdecorator function incalculator.py1def check_input(function): 2 def wrapper(first_input, second_input): 3 if isinstance( 4 first_input, 5 (dict, set, list, tuple, str, bool) 6 ) or isinstance( 7 second_input, 8 (dict, set, list, tuple, str, bool) 9 ) or first_input is None: 10 return 'brmph?! Numbers only. Try again...' 11 return function(first_input, second_input) 12 return wrapperSUBFAILED(operation='add', bad_input=None) ... - TypeError: unsupported operand type(s) for +: 'float' and 'NoneType' SUBFAILED(operation='subtract', bad_input=None) ... - TypeError: unsupported operand type(s) for -: 'float' and 'NoneType' SUBFAILED(operation='multiply', bad_input=None) ... - TypeError: unsupported operand type(s) for *: 'float' and 'NoneType' SUBFAILED(operation='divide', bad_input=None) ... - TypeError: unsupported operand type(s) for /: 'float' and 'NoneType'I add a condition for when the second input is None to the if statement in
check_inputincalculator.py1def check_input(function): 2 def wrapper(first_input, second_input): 3 if isinstance( 4 first_input, 5 (dict, set, list, tuple, str, bool) 6 ) or isinstance( 7 second_input, 8 (dict, set, list, tuple, str, bool) 9 ) or first_input is None or second_input is None: 10 return 'brmph?! Numbers only. Try again...' 11 return function(first_input, second_input) 12 return wrapperthe test passes
REFACTOR: make it better
I change the name of test_calculator_sends_message_when_input_is_not_a_number to test_calculator_sends_message_when_inputs_are_not_numbers in
test_calculator.py136 def test_calculator_sends_message_when_inputs_are_not_numbers(self): 137 for bad_input in ( 138 None, 139 True, False, 140 str(), 'text', 141 tuple(), (0, 1, 2, 'n'), 142 list(), [0, 1, 2, 'n'], 143 set(), {0, 1, 2, 'n'}, 144 dict(), {'key': 'value'}, 145 ): 146 for operation in self.calculator_tests: 147 with self.subTest( 148 operation=operation, 149 bad_input=bad_input, 150 ): 151 self.assertEqual( 152 src.calculator.__getattribute__(operation)( 153 bad_input, a_random_number() 154 ), 155 'brmph?! Numbers only. Try again...' 156 ) 157 self.assertEqual( 158 src.calculator.__getattribute__(operation)( 159 a_random_number(), bad_input 160 ), 161 'brmph?! Numbers only. Try again...' 162 ) 163 164 def test_calculator_functions(self):I make a variable for the bad inputs, to remove repetition in
calculator.py1def check_input(function): 2 def wrapper(first_input, second_input): 3 bad_inputs = (dict, set, list, tuple, str, bool) 4 if isinstance(I use the new variable in the if statement
1def check_input(function): 2 def wrapper(first_input, second_input): 3 bad_inputs = (dict, set, list, tuple, str, bool) 4 if isinstance( 5 first_input, bad_inputs 6 # (dict, set, list, tuple, str, bool) 7 ) or isinstance( 8 second_input, bad_inputs 9 # (dict, set, list, tuple, str, bool) 10 ) or first_input is None or second_input is None: 11 return 'brmph?! Numbers only. Try again...' 12 return function(first_input, second_input) 13 return wrapperthe test is still green
I add a for loop
11 return 'brmph?! Numbers only. Try again...' 12 for argument in (first_input, second_input): 13 if ( 14 isinstance(argument, bad_inputs) 15 or 16 argument is None 17 ): 18 return 'brmph?! Numbers only. Try again...' 19 return function(first_input, second_input) 20 return wrapperstill green
I remove the other if statements
1def check_input(function): 2 def wrapper(first_input, second_input): 3 bad_inputs = (dict, set, list, tuple, str, bool) 4 for argument in (first_input, second_input): 5 if ( 6 isinstance(argument, bad_inputs) 7 or 8 argument is None 9 ): 10 return 'brmph?! Numbers only. Try again...' 11 return function(first_input, second_input) 12 return wrappergreen
close the project
I close
test_calculator.py,test_calculator_website.py,calculator.py,website.pyandindex.htmlin the editorI click in the first terminal, then use q on the keyboard to leave the tests. The terminal goes back to the command line
I change directory to the parent of
calculatorcd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectoryI click in the second terminal, then use ctrl+c on the keyboard to close the web server. The terminal goes back to the command line
review
I ran tests to show that
I can can make a website with `Flask`_
I can make routes, handle forms, and use the calculator program I made earlier
The website shows error message from the calculator when the user tries to divide by
0The website makes sure the user can only use numbers
My tests make sure that if someone uses the program and does not send numbers, they get a message
code from the chapter
what is next?
Your magic powers are growing. You know
Would you like to see another way to make a website for the Calculator?
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