Skip to content

[中文] Why PyWebIO?

WangWeimin edited this page Nov 12, 2021 · 9 revisions

Why PyWebIO?

This article is written for developers with experience in web development and presents the disadvantages of traditional web development and the advantages of PyWebIO in a specific scenario. If you do not have experience in web development, you can ignore the web development related parts of this article.

Difficulties in migrating scripts to the Web

When you are writing some toy programs, you may encounter such a dilemma: a function can be quickly implemented through a script (terminal program), but for the convenience of use, you want to make it be a Web application.

There are several difficulties to migrate a terminal script to a Web application:

  1. Need to write additional front-end code to create web pages.
    Generally, at least two pages should be written, one is the input form and the other is the result page. If the input in the original script cannot be integrated into a form (for example, some input items need to be determined based on the result of some input items), then more form pages need to be written.

  2. Due to the stateless of the HTTP protocol, you need to pass state between various pages by yourself, and additional code is needed to check the validation of the state.
    For example, in Web development, the session mechanism of back-end or the <input type="hidden"> mechanism of front-end are generally used to pass the state between pages.

  3. Web application cannot achieve real-time output in a single HTTP request, which also increases the difficulty of converting script into Web service.
    For example, if the console program needs to perform some time-consuming operations, it can block in the main thread and show the progress by printing some logs. In web applications, time-consuming tasks usually need to be completed asynchronously, and the front-end page needs to be polled regularly to display real-time state of the task.

Next, we will explain this with an example.

An example

Let's say we need to implement a question and answer game, the rules of the game are: each time you get a random question, answer the current question correctly to proceed to the next question, when you answered a wrong question, the game ends, and then the game shows the cumulative number of questions you answered correctly.

Question data is provided in the following format:

questions = [
    {'question': 'What is a correct syntax to output "Hello World" in Python?',
     'options': ['echo("Hello World");', 'print("Hello World")', 'echo "Hello World"'],
     'answer': 1},
    ...
]

Next, we start to implement the game as a terminal program, a web application, and a PyWebIO application, respectively.

Terminal program

When you write the game as a terminal program, the code can be very straightforward. Here is the Python implementation:

from random import shuffle

questions = [...]

shuffle(questions)
for cnt, q in enumerate(questions):
    print('Question-%s: %s' % (cnt + 1, q['question']))
    print('Options: \n', '\n'.join(f'[{idx}] {opt}' for idx, opt in enumerate(q['options'])))
    answer = input('Input your answer (only the number of the option):')
    if answer != str(q['answer']):
        print(f'Game Over. You have passed {cnt} questions.')
        break
else:
    print(f'Congratulations! You have passed all the {len(questions)} questions.')

As you can see, the code is very clear. However, the UI of the application is also very poor:

Console app

Besides, this application can not be used by others outside the local machine, in order to solve this problem, we need the Web application.

Web application

If you want to implement this game as a web application, more work needs to be done.

Specifically, you need to write the html page and the backend interface separately. A total of 2 html pages are needed, one to show the question and options and one to show the message after the game is over.

Two backend API are needed for displaying questions and receiving answers submitted by users.

The Flask implementation is shown below.

from flask import Flask, request, session, render_template
from random import shuffle

app = Flask(__name__)

questions = [...]

@app.route('/')
def index():
    question_idxs = list(range(len(questions)))
    shuffle(question_idxs)
    session['question_idxs'] = question_idxs
    session['question_cnt'] = 0

    current = questions[question_idxs[0]]
    return render_template('question.html', question=current['question'],
                           options=current['options'], cnt=0)


@app.route('/submit_answer', methods=['POST'])
def submit_answer():
    cnt = session['question_cnt']
    question_idxs = session['question_idxs']
    current_question = questions[question_idxs[cnt]]
    answer = request.form['answer']
    if answer != str(current_question['answer']):
        return f'Game Over. You have passed {cnt} questions.'

    session['question_cnt'] += 1
    if session['question_cnt'] >= len(questions):
        return f"Congratulations! You have passed all the {len(questions)} questions."

    next_question = questions[question_idxs[cnt + 1]]
    return render_template('question.html', question=next_question['question'],
                           options=next_question['options'], cnt=cnt + 1)


if __name__ == '__main__':
    app.run()

The question.html template has the following content. Also, for simplicity, we return the end-of-game message directly as a string.

question.html template
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Question-{{cnt+1}}</title>
</head>
<body>
<b>Question-{{cnt+1}}:</b><i>{{ question }}</i><br>
<b>Options:</b><br>
<form action="/submit_answer" method="post">
    {% for opt in options %}
    <input type="radio" name="answer" id="options-{{loop.index0}}" value="{{loop.index0}}">
    <label for="options-{{loop.index0}}">{{opt}}</label>
    {% endfor %}
    <br>
    <input type="submit">
</form>
</body>
</html>

In the flask app above, we used session to maintain state. However, we need to be very careful about maintaining session, otherwise the application is prone to vulnerabilities.

For example, did you find that there are actually some problems in this flask app?

Issue 1: Users can submit answers to the same question more than once.

According to the rules of the game, the game ends after the user submits an incorrect answer, but what happens at this point if the user continues to submit data to the /submit_answer interface? In our application, the application will accept the submissions from the user! This also means that if the user answers incorrectly, they can retry other answers until they are correct. This is a very serious vulnerability for our application.

Issue 2: A malicious user may submit data directly to the /submit_answer interface without getting a question, which will raise an exception.

While it's true that this won't cause too serious a problem in this application, in some scenarios it may lead to problems such as the database storing empty data.

To solve these two problems, we need to reset the session after the game ends and add additional session status checking code. The code is modified as follows:

@app.route('/submit_answer', methods=['POST'])
def submit_answer():
+   if 'question_cnt' not in session:
+       return "Invalid state!"  
    cnt = session['question_cnt']
    question_idxs = session['question_idxs']
    current_question = questions[question_idxs[cnt]]
    answer = request.form['answer']
    if answer != str(current_question['answer']):
+       del session['question_cnt']
+       del session['question_idxs'] 
        return f'Game Over. You have passed {cnt} questions.'

    session['question_cnt'] += 1
    if session['question_cnt'] >= len(questions):
        return f"Congratulations! You have passed all the {len(questions)} questions."

    next_question = questions[question_idxs[cnt + 1]]
    return render_template('question.html', question=next_question['question'],
                           options=next_question['options'], cnt=cnt + 1)

After taking great care to write four times the amount of code as the terminal program, we finally got the web version of the game.

Flask app

The only thing is the UI is still ugly. Don't worry, we can beautify the UI by writing some CSS styles, if you can and are willing to write CSS.

So far, we have seen the huge difference between the implementation of the same logic in a terminal program and a Web program.

However, do we have to suffer such a headache to build web applications? The answer is no, because you now have a new option for building Web applications - PyWebIO.

PyWebIO's solution

PyWebIO can quickly convert scripts into web app by replacing the input and output functions in the terminal program version with the input and output functions provided by PyWebIO:

from random import shuffle

from pywebio import start_server
from pywebio.input import *
from pywebio.output import *

questions = [...]

def main():
    idxs = list(range(len(questions)))
    shuffle(idxs)
    for cnt, idx in enumerate(idxs):
        q = questions[idx]
        answer = radio('Question-%s: %s' % (cnt + 1, q['question']), options=q['options'])
        if answer != q['options'][q['answer']]:
            put_error(f'Game Over. You have passed {cnt} questions.')
            break
    else:
        put_success(f'Congratulations! You have passed all the {len(questions)} questions.')


if __name__ == '__main__':
    start_server(main, port=8080)

The PyWebIO version has the same clean code as the terminal version, while allowing multiple users and a more user-friendly interface than the Flask version (not to mention the comparison with the terminal version).

pywebio app

At the same time, when writing web services using PyWebIO, there are no problems as described above, because the code logic of PyWebIO is the same way of writing a console application. Furthermore, PyWebIO can output a wider variety of content (e.g., images, charts, etc.) than a terminal application, making it ideal for quickly building interactive Web applications.

Clone this wiki locally