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 arithmetic
    
  • Gemini returns this code for the backend app.py

    from 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 arithmetic
    
  • Grok 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 arithmetic
    
  • ChatGPT returns this code for a file named calculator.py

    
    
  • ChatGPT 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 arithmetic
    
  • Grok returns this code for the backend app.py

    
    
  • Grok returns this code for the frontend templates/index.html

    
    
  • it 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 arithmetic
    
  • Grok returns this code for the backend app.py

    
    
  • Grok returns this code for the frontend templates/index.html

    
    
  • it 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 calculator folder

    cd calculator
    

    the terminal shows I am in the calculator folder

    .../pumping_python/calculator
    
  • I make a new test file for the website

    touch tests/test_calculator_website.py
    
  • I add Flask_ to the requirements.txt file

    echo "flask" >> requirements.txt
    

    Flask_ 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 5 items
    
    tests/test_calculator.py .....                                [100%]
    
    ======================== 5 passed in X.YZs =========================
    

test_home_page


RED: make it fail


  • I open test_calculator_website.py from the tests folder in the editor

  • I add a test to test_calculator_website.py

    1import unittest
    2
    3
    4class TestCalculatorWebsite(unittest.TestCase):
    5
    6    def test_home_page(self):
    7        client = src.website.app.test_client()
    

    the terminal shows NameError

    NameError: name 'src' is not defined
    
  • I 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



REFACTOR: make it better


  • I add an assertion to test_home_page in test_calculator_website.py

     7    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# AssertionError
    
  • I change the expectation in the assertion

    10        self.assertEqual(response.status_code, 404)
    

    the test passes.

  • 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 != 200
    
  • I add a function for the homepage in website.py

    1import 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 != 200
    

    it also shows `jinja2.exceptions.TemplateNotFound`_

    jinja2.exceptions.TemplateNotFound: index.html
    

    I have to make a template file for index.html

    • @app.route is 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 as index.html

  • I add `jinja2.exceptions.TemplateNotFound`_ to the list of Exceptions seen

    # Exceptions seen
    # NameError
    # ModuleNotFoundError
    # AttributeError
    # AssertionError
    # jinja2.exceptions.TemplateNotFound
    
  • I add a new folder to the src folder named templates, the terminal still shows the same Exception

  • I add a new file in the templates folder named index.html, the terminal still shows the same Exception

  • I go back to test_calculator_website.py then 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 --debug
    

    the 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-GHI
    

    it might also show a dialog box like this, and I click on Open in Browser

    Confirm you want to view Flask Development Server

    or I use ctrl/option on the keyboard and click on http://127.0.0.1:5000 with 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.py

     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,
    13            '<h1>Calculator</h1>'
    14        )
    

    the terminal shows AssertionError

    AssertionError: b'' != '<h1>Calculator</h1>'
    
    • there is nothing in src/templates/index.html

    • b'' is the empty `bytes object`_

    • `bytes objects`_ are like strings , they have a b before the quotes, for example

      b'single quote bytes'
      b'''triple single quote bytes'''
      b"double quote bytes"
      b"""triple double quote bytes"""
      
  • I open index.html from the templates folder in the editor, then add some HTML

    1<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 make HEADING a heading

  • I go back to test_calculator_website.py and use ctrl+s on the keyboard to run the test, the terminal shows AssertionError

    AssertionError: 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 shows Calculator

    Calculator Header

    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.


GREEN: make it pass


  • I add a new function to website.py

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

    the terminal shows NameError

    NameError: name 'calculator' is not defined. Did you mean: 'calculate'?
    
  • I add an import statement for the calculator module at the top of the file

    1import calculator
    2import flask
    

    the terminal shows ModuleNotFoundError

    ModuleNotFoundError: No module named 'calculator
    

    because the test cannot find the calculator.py module

  • I change the import statement

    1import src.calculator
    2import flask
    

    the test passes and the terminal for the website shows ModuleNotFoundError

    ModuleNotFoundError: No module named 'src'
    

    this is a problem

    • website.py is in the src folder with calculator.py so import src.calculator will not work for the website

    • test_calculator_website.py is in the tests folder so import calculator will 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 flask
    

    the 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 flask
    

    I 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 flask
    
  • I use pathlib to add the path for calculator.py to the `system path`_ which is a list of strings where Python looks for modules when I write an import statement

    1import pathlib
    2import sys
    3sys.path.insert(
    4    0, str(pathlib.Path(__file__).resolve().parent)
    5)
    6
    7import calculator
    8import flask
    

    the test passes and the terminal for the browser shows no errors

    • the path to calculator.py is now correct for website.py and test_calculator.py

    • sys.path.insert uses the insert method of lists to place the src folder as the first item in the list for Python to look for modules

    • pathlib.Path(__file__).resolve().parent returns the parent of the current file - src in this case

    • __file__ is a variable with the name of the file website.py in this case

    • the route to /calculate now exists


REFACTOR: make it better


  • I add an assertion to make sure I get the right result back in test_calculator_website.py

    13    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 0 and 1 are getting converted to something the calculator does not like

  • I change first_input to a float in website.py

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

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for +: 'float' and 'str'
    
  • I change second_input to a float

    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
    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.py

    16    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_calculator module

    1import src.website
    2import tests.test_calculator
    3import unittest
    
  • I point a variable to the result of a call to the a_random_number function of test_calculator

    17    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 syntax
    

    I 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 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 y to the f-string in the expectation

    31        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 seen
    
  • I add a key to the data dictionary

    32                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 result

    34    # 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.py

    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
    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.html in the templates folder in the src folder

    1  <h1>Calculator</h1>
    2  <form method="post" action="/calculate">
    3  </form>
    
    • <form></form> is an HTML tag (enclosure) for making forms

    • method="post" means the form will use the `POST request method`_

    • action="/calculate" means the form will send the data to the calculate function in website.py

  • I go to the website and click refresh, there is no change

  • I click on test_calculator_website.py and use ctrl+s on the keyboard to run the tests again, the terminal shows AssertionError

    AssertionError: 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.html in the templates folder in the src folder

    2<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

    Calculator form with first input
  • I use ctrl+s on the keyboard in test_calculator_website.py to run the tests, still green

  • I add another input for the second number in index.html

    2<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

    Calculator form with second input
  • I use ctrl+s on the keyboard in test_calculator_website.py to run the tests, green

  • I add options for the operation in index.html

    2<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

    Calculator form with first operation
  • I use ctrl+s on the keyboard in website.py to run the tests, they are still green

  • I want the operation to show up between the 2 numbers, I change the order in index.html

    2<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

    Calculator form with first operation redordered
  • I use ctrl+s on the keyboard in website.py to run the tests. Green

  • I add the other operations to index.html

     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</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

    Calculator form with calculate button
  • I enter 10000 as the first number and 20000 as the second number, with * as the operation

    Calculator form first calculation

    I click calculate and the browser shows

    Calculator first result

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.py from the src folder

  • I add an exception handler to the divide function in calculator.py

    17@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.py

    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            (
    69                f'<h2>{x} / 0.0 = '
    70                'brmph?! I cannot divide by 0. Try again...</h2>'
    71            )
    72        )
    73
    74
    75# Exceptions seen
    

    the 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 client variable and the commented line

    11    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 client variable

    19    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 client variable

    54    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 seen
    

    green


  • 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 Symbol to change x to self.x

    20    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.x

    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):
    
  • I use the Rename Symbol feature to change x to self.x in test_website_handling_zero_division_error

    54    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.py from the tests folder

  • I 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):
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for /: 'float' and 'dict
    

    for 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_input decorator function in calculator.py

     1def 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 wrapper
    

    the terminal shows TypeError

    SUBFAILED(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_input in calculator.py

     1def 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 wrapper
    

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

    136    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.py

    1def 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 wrapper
    

    the 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 wrapper
    

    still 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 wrapper
    

    green


close the project

  • I close test_calculator.py, test_calculator_website.py, calculator.py, website.py and index.html in the editor

  • I 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 calculator

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory

  • I 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 0

  • The 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

Do you want to see all the CODE I typed in this 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