Flask: Web Forms

Updated April 11, 2024

Previous:

  1. Flask intro: A very simple Flask app

  2. Flask, part 2: Values in routes; using an API

  3. Flask templates: Write HTML templates for a Flask app

  4. Flask: Deploy an app: How to put your finished app online

Code for this chapter is here.

In the Flask Templates chapter, we built a functioning Flask app. In this chapter, we’ll explore how to add functional web forms to a similar app.

Actors app screenshots

Flask forms app example (flask_actors_app):

Introduction

Flask has an extension that makes it easy to create web forms.

WTForms is “a flexible forms validation and rendering library for Python Web development.” With Flask-WTF, we get WTForms in Flask.

  • WTForms includes security features for submitting form data.

  • WTForms has built-in validation techniques.

  • WTForms can be combined with Bootstrap to help us make clean-looking, responsive forms for mobile and desktop screens.

  • WTForms is a Python library.

  • Flask-WTF is a Flask extension that brings WTForms into Flask.

Read the documentation for Flask-WTF.

Setup for using forms in Flask

We will install the Flask-WTF extension to help us work with forms in Flask. There are many extensions for Flask, and each one adds a different set of functions and capabilities. See the list of Flask extensions for more.

In Terminal, change into your Flask projects folder and activate your virtual environment there. Then, at the command prompt — where you see $ (Mac) or C:\Users\yourname> (Windows )—

pip install Flask-WTF

We will also install the Bootstrap-Flask extension to provide Bootstrap styles for our forms.

pip install bootstrap-flask

This installation is done only once in any virtualenv. It is assumed you already have Flask installed there.

Imports for forms with Flask-WTF and Bootstrap-Flask

You will have a long list of imports at the top of your Flask app file:

from flask import Flask, render_template, redirect, url_for
from flask_bootstrap import Bootstrap5

from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length

Note as always that Python is case-sensitive, so upper- and lowercase must be used exactly as shown. The wtforms import will change depending on your form’s contents. For example, if you have a SELECT element, you’ll need to import that. See a simplified list of WTForms form field types or further explanation in the WTForms documentation.

Set up a form in a Flask app

After the imports, these lines follow in the app script:

app = Flask(__name__)
app.secret_key = 'tO$&!|0wkamvVia0?n$NqIRVWOG'

# Bootstrap-Flask requires this line
bootstrap = Bootstrap5(app)
# Flask-WTF requires this line
csrf = CSRFProtect(app)

Flask allows us to set a “secret key” value. This value is used to prevent malicious hijacking of your form from an outside submission. A better way to do it:

import secrets
foo = secrets.token_urlsafe(16)
app.secret_key = foo

Flask-WTF’s FlaskForm will automatically create a secure session with CSRF (cross-site request forgery) protection if this key-value is set and the csrf variable is set. Don’t publish an actual key on GitHub!

You can read more about the secret key in this StackOverflow post.

Configure the form

Next, we configure a form that inherits from Flask-WTF’s class FlaskForm. Python style dictates that a class starts with an uppercase letter and uses camelCase, so here our new class is named NameForm (we will use the form to search for a name).

In the class, we assign each form control to a unique variable. This form has only one text input field and one submit button.

Every form control must be configured here.

class NameForm(FlaskForm):
    name = StringField('Which actor is your favorite?', validators=[DataRequired(), Length(10, 40)])
    submit = SubmitField('Submit')

Learn more about classes in Python here.

If you had more than one form in the app, you would define more than one new class in this manner.

Note that StringField and SubmitField were imported at the top of the file. If we needed other form-control types in this form, we would need to import those also. See a simplified list of WTForms form field types or further explanation in the WTForms documentation.

Note that several field types (such as RadioField and SelectField) must have an option choices=[] specified. Within the list, each choice is a pair in this format: ('string-form-variable-name', 'string-label-text').

category = RadioField('Choose a detail to search:', validators=[InputRequired(message=None)],
choices=[ ('President', 'President\'s Name, e.g. John'), ('Home-state', 'Home State, e.g. Virginia'),
('Occupation', 'Occupation, e.g. Lawyer'), ('College', 'College, e.g. Harvard')] )

Here is a live form page shown beside the rendered source code for choices.

Live form page shown beside rendered source code for choices

For more help with the FlaskForm class, see this Bootstrap-Flask page. It shows great examples with the exact code needed.

WTForms also has a long list of validators we can use. The DataRequired() validator prevents the form from being submitted if that field is empty. Note that these validators must also be imported at the top of the file. Validators and custom validators are discussed further in the WTForms documentation.

Put the form in a route function

Now we will use the form in a Flask route:

../python_code_examples/flask/actors_app/actors.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@app.route('/', methods=['GET', 'POST'])
def index():
    names = get_names(ACTORS)
    # you must tell the variable 'form' what you named the class, above
    # 'form' is the variable name used in this template: index.html
    form = NameForm()
    message = ""
    if form.validate_on_submit():
        name = form.name.data
        if name.lower() in names:
            # empty the form field
            form.name.data = ""
            id = get_id(ACTORS, name)
            # redirect the browser to another route and template
            return redirect( url_for('actor', id=id) )
        else:
            message = "That actor is not in our database."
    return render_template('index.html', names=names, form=form, message=message)

A crucial line is where we assign our configured form object to a new variable:

form = NameForm()

We must also pass that variable to the template, as seen in the final line above.

Be aware that if we had created more than one form class, each of those would need to be assigned to a unique variable. (They cannot all be named form.)

Put the form in a template

Before we break all that down and explain it, let’s look at the code in the template index.html:

../python_code_examples/flask/actors_app/templates/index.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{% extends 'base.html' %}
{% from 'bootstrap5/form.html' import render_form %}

{% block title %}
Best Movie Actors
{% endblock %}


{% block content %}

<!--
	TIPS about using Bootstrap-Flask:
	https://github.com/helloflask/bootstrap-flask
	https://bootstrap-flask.readthedocs.io/
-->

<div class="container">
  <div class="row">
    <div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">

      <h1 class="pt-5 pb-2">Welcome to the best movie actors Flask example!</h1>

      <p class="lead">This is the index page for an example Flask app using Bootstrap and WTForms. Note that <em>only 100 actors</em> are in the data source. Partial names are not valid.</p>

      {{ render_form(form) }}

      <p class="pt-5"><strong>{{ message }}</strong></p>

    </div>
  </div>
</div>

{% endblock %}

Where is the form? This is the amazing thing about Flask-WTF — by configuring the form as we did in the Flask app, we can generate a form with Bootstrap styles in HTML using nothing more than the template you see above. Line 25 is the form.

Drawing of magician pulling rabbit from hat

Note that in the Flask route function, we passed the variable form to the template index.html:

return render_template('index.html', names=names, form=form, message=message)

When you use {{ render_form(form) }}, the argument inside the parentheses must be the variable that represents the form you created in the app.

form = NameForm()

We discussed the configuration of NameForm above.

A quick note about Bootstrap in Flask

There’s more about this in the Resources section at the bottom of this page — but to summarize briefly:

  • You pip-installed Bootstrap-Flask in your Flask virtual environment.

  • You wrote from flask_bootstrap import Bootstrap5 at the top of the Flask app file.

  • Below that, you wrote bootstrap = Bootstrap5(app) in the Flask app file.

  • In the Flask template index.html, the top line is: {% from 'bootstrap5/form.html' import render_form %}

That combination of four things has embedded Bootstrap 5 in this app and made {{ render_form() }} possible.

Note that it is possible to use Bootstrap-Flask without any forms! The actors app demonstrates how the usual Bootstrap classes such as container and row can be used in Flask templates.

Examining the route function

Before reading further, try out a working version of this app. The complete code for the app is on GitHub (updated April 2024).

  1. You type an actor’s name into the form and submit it. Try Willem Dafoe, Viola Davis, Morgan Freeman, or Kate Winslet (there are only 100 names).

  2. If the actor’s name is in the data source (ACTORS), the app loads a detail page for that actor. (Photos of bears 🐻 stand in for real photos of the actors.)

  3. Otherwise, you stay on the same page, the form is cleared, and a message tells you that actor is not in the database.

../python_code_examples/flask/actors_app/actors.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@app.route('/', methods=['GET', 'POST'])
def index():
    names = get_names(ACTORS)
    # you must tell the variable 'form' what you named the class, above
    # 'form' is the variable name used in this template: index.html
    form = NameForm()
    message = ""
    if form.validate_on_submit():
        name = form.name.data
        if name.lower() in names:
            # empty the form field
            form.name.data = ""
            id = get_id(ACTORS, name)
            # redirect the browser to another route and template
            return redirect( url_for('actor', id=id) )
        else:
            message = "That actor is not in our database."
    return render_template('index.html', names=names, form=form, message=message)

First we have the route, as usual, but with a new addition for handling form data: methods.

@app.route('/', methods=['GET', 'POST'])

Every HTML form has two possible methods, GET and POST. GET simply requests a response from the server. POST, however, sends a request with data attached in the body of the request; this is the way most web forms are submitted.

This route needs to use both methods because when we simply open the page, no form was submitted, and we’re opening it with GET. When we submit the form, this same page is opened with POST if the actor’s name (the form data) was not found. Thus we cannot use only one of the two options here.

def index():
    names = get_names(ACTORS)

At the start of the route function, we get the data source for this app. It happens to be in a list named ACTORS, and we get just the names by running a function, get_names(). The function was imported from the file named modules.py.

form = NameForm()
message = ""

We assign the previously configured form object, NameForm(), to a new variable, form. This has been discussed above.

We create a new, empty variable, message.

if form.validate_on_submit():
    name = form.name.data

validate_on_submit() is a built-in WTForms function, called on form (our variable). If it returns True, the following commands and statements in the block will run. If not, the form is simply not submitted, and invalid fields are flagged. It will return True if the form was filled in and submitted.

form.name.data is the contents of the text input field represented by name. Perhaps we should review how we configured the form:

class NameForm(FlaskForm):
   name = StringField('Which actor is your favorite?', validators=[DataRequired(), Length(10, 40)])
   submit = SubmitField('Submit')

That name is the name in form.name.data — the contents of which we will now store in a new variable, name. To put it another way: The variable name in the app now contains whatever the user typed into the text input field on the web page — that is, the actor’s name.

38
39
40
41
42
43
44
45
        if name.lower() in names:
            # empty the form field
            form.name.data = ""
            id = get_id(ACTORS, name)
            # redirect the browser to another route and template
            return redirect( url_for('actor', id=id) )
        else:
            message = "That actor is not in our database."

This if-statement is specific to this app. It checks whether the name (that was typed into the form) matches any name in the list names. If not, we jump down to else and text is put into the variable message. If name DOES match, we clear out the form, run a function called get_id() (from modules.py) and — important! — open a different route in this app:

return redirect( url_for('actor', id=id) )

Thus redirect( url_for('actor', id=id) ) is calling a different route here in the same Flask app script. (See actors.py, lines 48-57.) The redirect() function is specifically for this use, and we imported it from the flask module at the top of the app. We also imported url_for(), which you have seen previously used within templates.

As far as using forms with Flask is concerned, you don’t need to worry about the actors and their IDs, etc. What is important is that the route function can be used to evaluate the data sent from the form. We check to see whether it matched any of the actors in a list, and a different response will be sent based on match or no match.

Any kind of form data can be handled in a Flask route function.

You can do any of the things that are typically done with HTML forms — handle usernames and passwords, write new data to a database, create a quiz, etc.

The final line in the route function calls the template index.html and passes three variables to it:

return render_template('index.html', names=names, form=form, message=message)

Conclusion

Flask-WTF provides convenient methods for working with forms in Flask. Forms can be built easily and also processed easily, with a minimum of code.

Adding Bootstrap-Flask ensures that we can build mobile-friendly forms with a minimum amount of effort.

Note that it is possible to build a customized form layout using Bootstrap styles in a Flask template, or to build a custom form with no Bootstrap styles. In either of those two cases, you cannot use {{ render_form(form) }} but would instead write out all the form code in your Flask template as you would in a normal HTML file. To take advantage of WTForms, you would still create the form class with FlaskForm in the same way as shown above.

An example is the demo Flask app Books Hopper, which includes four separate Bootstrap forms:

  • a login form

  • a registration form

  • a search form

  • a form for writing a book review and selecting a rating

Books Hopper screenshot

Bootstrap 4 was used in all templates in the Books Hopper app, but Bootstrap-Flask was not. Bootstrap styles were all coded in the usual ways.

Resources

.