Handling Forms in Flask with Flask-WTF
Tags: flask, python, software
Published: 2020-01-09 08:22:44

---
title: Handling Forms in Flask with Flask-WTF
published: true
date: 2018-08-15 10:25:00 UTC
tags: Flask,Python,Software
series: Build Flask Python Apps
canonical_url: https://hackersandslackers.com/forms-in-flask-wtforms
---

 ![Handling Forms in Flask with Flask-WTF](https://res-1.cloudinary.com/hackers/image/upload/q_auto:best/v1/2019/11/flask-wtforms.jpg)

Let's talk about something that everybody hates: forms. The only thing more painful than filling out a form is creating one, much less a functional one with feedback. If the thought of building functional forms doesn't sicken you, you're probably into some freaky shit. Call me.

If you don't have a pressing need to create forms in Flask, I won't be offended if you decide to ditch this post. Feel free to scroll endlessly through Instagram, but know this: handling form authentication and data submission is the _pinnacle of app development_. This is where front-end meets back-end: a dance of data where a person's keystrokes result in meaningful action. He who creates forms is a harbinger of a golden age: a hero who brings us to the pinnacle of Western technology. Then again, there's always Instagram.

Both Flask and Django have relied heavily on a single Python library by the name of [WTForms](https://wtforms.readthedocs.io/en/stable/). WTForms has strangely remained the undisputed library for handling forms for years without consequence: the library is easy to use, and nobody has maliciously hijacked the library to steal everybody's personal information or Bitcoin. That's the real difference between the Python and Javascript ecosystems. The flavor of WTForms is actually a Flask plugin called [Flask-WTF](https://flask-wtf.readthedocs.io/), which provides a few nice perks for Flask users. Truthfully, Flask-WTF isn't far from the original library at all (this tutorial had actually originally used **WTForms** instead of **Flask-WTF** , and the source code somehow remained almost exactly the same).

## Laying the Groundwork

I know you're lazy so copy+paste the line below so we can get on with it:

```shell
$ pip3 install flask flask-wtf
```
<figcaption>Install dependencies</figcaption>

Before laying down some code snippets for you to mindlessly copy+paste, it helps to understand what we're about to throw down. Here's a look at our structure before anybody gets lost:

```shell
/flask-wtforms-tutorial
├─ /templates
├─ /static
├─ app.py
├─ config.py
└─ forms.py
```
<figcaption>Project structure</figcaption>

Creating a functioning form has a minimum of three parts:

1. **Routes** for determining what users should be served when visiting a URL within our app. We're going to keep things simple by defining our routes in a single `app.py` file.
2. **Form classes** are Python models that determine the data our forms will capture, as well as the logic for validating whether or not a user has adequately completed a form when they attempt to submit. These classes are going to live in `forms.py`.
3. **Jinja templates** will render the actual HTML forms that users will see. As we'll soon discover, Flask makes bridging the gap between Python models and HTML forms easy.

We'll start off by creating our form logic in `forms.py` **.**

## What The Form

**Flask-WTF** comes packaged with **WTForms** as a dependency: the two libraries are intended to be used together, as opposed to one obfuscating the other. The core of Flask-WTF is a class called `FlaskForm`, which we extend with the form classes we define ourselves. This is mostly for convenience, as creating classes that extend `FlaskForm` inherently gives us some out-of-the-box logic related to validation, etc.:

```python
from flask_wtf import FlaskForm
```
<figcaption>forms.py</figcaption>

Our form is going to need input fields, as well as a way of validating the content of those fields once submitted. We can import field types directly from the `wtforms` package, and validators from the `wtforms.validators` package:

```python
from flask_wtf import FlaskForm, 
from wtforms import StringField, TextField, SubmitField
from wtforms.validators import DataRequired, Length
```
<figcaption>forms.py</figcaption>

We're going to create a single Python class per form. We'll start by creating a straightforward "contact us" form:

```python
"""Form object declaration."""
from flask_wtf import FlaskForm
from wtforms import StringField, TextField, SubmitField
from wtforms.validators import DataRequired, Length


class ContactForm(FlaskForm):
    """Contact form."""
    name = StringField(
        'Name',
        [DataRequired()]
    )
    email = StringField(
        'Email',
        [
            Email(message=('Not a valid email address.')),
            DataRequired()
        ]
    )
    body = TextField(
        'Message',
        [
            DataRequired(),
            Length(min=4,
            message=('Your message is too short.'))
        ]
    )
    recaptcha = RecaptchaField()
    submit = SubmitField('Submit')
```
<figcaption>forms.py</figcaption>

We've created a form with 3 input fields ( `name`,  `email`, `body` ) as well as a `submit` field (which is really just a button). Each of our input fields consists of the following pieces:

- **Type of Input**: These are the field types we imported from `wtforms`,_ where `StringField` is a single-line text field, `TextField` is a multiline textarea, and so forth. WTForms has a strong collection of input types which includes inputs for things like passwords, date-pickers, multi-select drop-downs, and so forth. We'll get more into these later.
- **Label**: The first parameter passed to a "field" object is the field's "label," AKA the human-readable name for each field. Labels will carry through to our end users, so we should name our input fields properly.
- **Validators**: A _validator_ is a restriction put on a field that must be met for the user's input to be considered valid. These are restrictions, such as ensuring a password is a minimum of 8 characters long. An input field can have multiple validators. If the user attempts to submit a form where _any_ field's validators are not fully met, the form will fail and return an error to the user.
- **Error Message**: Any time a validator is not met, we need to tell the user what went wrong. Thus, every validator has an error message.

Admittedly, form classes aren't always easy to decipher at first glance due to their complexity. Here's a quick blueprint of what each input field is comprised of:

```python
[VARIABLE] = [FIELD TYPE]('[LABEL]', [
        validators=[VALIDATOR TYPE](message=('[ERROR MESSAGE'))
    ])
```

## Serving Forms In Flask Routes

So we have a form class, but we haven't even created a Flask app yet. Let's take care of that, and let's create a route for our contact form while we're at it:

```python
from flask import Flask, url_for, render_template, redirect
from forms import ContactForm

app = Flask( __name__ , instance_relative_config=False)
app.config.from_object('config.Config')

@app.route('/', methods=('GET', 'POST'))
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        return redirect(url_for('success'))
    return render_template('index.jinja2', form=form)
```
<figcaption>app.py</figcaption>

It seems simple enough, but there's more happening here than it seems at first glance. Our `contact` route accepts both **GET** and **POST** requests; normally we handle routes like this by using a conditional on Flask's `request` object ( such as `if request.method == 'POST'` ), but we haven't even imported the `request` object at all! Because we used Flask-WTF's `FlaskForm` base class to create `ContactForm()`, we're able to simplify routes containing form logic into two scenarios: one where the user submitted a valid, and one for _everything_ else. We're able to do this because our form now has a built-in method called `validate_on_submit()`, which detects if a request is _both_ a POST request and a valid request.

When we do serve our form, we're doing so by passing our form variable into an `index.jinja2` Jinja template.

## Building Forms in Jinja

Writing anything resembling HTML is obviously the shittiest part of our lives. Jinja luckily reduces the pain a bit by making form handling easy:

```html
{% extends 'layout.jinja2' %}


{% block content %}

    <div class="form-wrapper">
      <h2 class="title">Contact</h2>

      <form method="POST" action="{{ url_for('contact') }}">

          <fieldset class="form-field">
              {{ form.name.label }}
              {{ form.name(size=20) }}
          </fieldset>

          <fieldset class="form-field">
              {{ form.email.label }}
              {{ form.email }}
          </fieldset>

          <fieldset class="form-field">
              {{ form.body.label }}
              {{ form.body }}
          </fieldset>

          {{ form.submit }}

      </form>
    </div>

{% endblock %}
```
<figcaption>contact.jinja2</figcaption>

This is truly as easy as it looks. We passed our form (named `form` ) to this template, thus `{{ form.name }}` is our form's _name_ field, `{{ form.email }}` is our form's _email_ field, and so forth. Let's see how it looks:

![Handling Forms in Flask with Flask-WTF](https://res-3.cloudinary.com/hackers/image/upload/q_auto:best/v1/2019/11/wtforms-no-error.gif)<figcaption>Submitting an invalid form.</figcaption>

Oh no... we haven't built in a way to display errors yet! Without any visual feedback, users have no idea _why_ the form is failing, or if it's even attempting to validate at all.

Form fields can handle having multiple validators, which is to say that a single field has several criteria it must meet before the provided data is considered valid. Since a field can have multiple validators, it could also potentially throw multiple errors if said validators are not satisfied. To handle this, we can use Jinja to loop through all errors for each of our fields (if they exist) and display all of them. Here's an example with how we'd handle errors on the **email** field alone:

```html
...

<fieldset class="form-field">
  {{ form.email.label }}
  {{ form.email }}
  {% if form.email.errors %}
    <ul class="errors">
      {% for error in form.email.errors %}
        <li>{{ error }}</li>
      {% endfor %}
    </ul>
  {% endif %}
</fieldset>

...
```
<figcaption>contact.jinja2</figcaption>

When a form in Flask fails, it executes the form action with metadata of the request provided. In our case, the action of our form reloads the page it lives on by default but sends along whichever form error messages were attached to the request. We can access form errors per field using `form.email.errors`, and since we know there might be multiple errors, we use `{% for error in form.email.errors %}` to loop through all errors for this single field.

Let's see how the full form looks now with proper error feedback handling:

```html
{% extends 'layout.jinja2' %}


{% block content %}

  <div class="form-wrapper">
    <h2 class="title">Contact</h2>

    <form method="POST" action="{{ url_for('signup') }}">

      <fieldset class="form-field">
      {{ form.name.label }}
      {{ form.name }}
        {% if form.name.errors %}
        <ul class="errors">
          {% for error in form.name.errors %}
            <li>{{ error }}</li>
          {% endfor %}
        </ul>
        {% endif %}
      </fieldset>

      <fieldset class="form-field">
        {{ form.email.label }}
        {{ form.email }}
        {% if form.email.errors %}
          <ul class="errors">
            {% for error in form.email.errors %}
              <li>{{ error }}</li>
            {% endfor %}
          </ul>
        {% endif %}
      </fieldset>

      <fieldset class="form-field">
        {{ form.body.label }}
        {{ form.body }}
        {% if form.body.errors %}
          <ul class="errors">
            {% for error in form.body.errors %}
              <li>{{ error }}</li>
            {% endfor %}
          </ul>
        {% endif %}
      </fieldset>

      {{ form.submit }}

      </form>
  </div>

{% endblock %}
```
<figcaption>contact.jinja2</figcaption>

Our simple form suddenly feels more complex, perhaps cumbersome, as we deal with the reality of form logic handling. We should not let this translate into complexity for our end user. On the contrary, the more robust we make our forms for catching edge-cases, the more usable a form will appear to be to our users, as long as we communicate errors in an effective visual manner:

![Handling Forms in Flask with Flask-WTF](https://res-2.cloudinary.com/hackers/image/upload/q_auto:best/v1/2019/11/wtforms-error.gif)<figcaption>Fields are now throwing error messages!</figcaption>

We can now see a proper error message being displayed when the user attempts to submit with a bogus email! You'll also notice the prompt which occurs when the user attempts to submit the form without filling the required fields: this prompt is actually built-in, and will occur for any field which contains the `DataRequired()` validator.

### Create a Signup Form

Let's explore Flask-WTF a little more by creating a more complicated form, such as an account sign-up form:

```python
from flask_wtf import FlaskForm, RecaptchaField
from wtforms import (
    StringField,
    TextAreaField,
    SubmitField,
    PasswordField,
    DateField,
    SelectField
)
from wtforms.validators import (
    DataRequired,
    Email,
    EqualTo,
    Length,
    URL
)

...

class SignupForm(FlaskForm):
    """Sign up for a user account."""
    email = StringField(
        'Email',
        [
            Email(message='Not a valid email address.'),
            DataRequired()
        ]
    )
    password = PasswordField(
        'Password',
        [
            DataRequired(message="Please enter a password."),
        ]
    )
    confirmPassword = PasswordField(
        'Repeat Password',
        [
            EqualTo(password, message='Passwords must match.')
        ]
    )
    title = SelectField(
        'Title',
        [DataRequired()],
        choices=[
            ('Farmer', 'farmer'),
            ('Corrupt Politician', 'politician'),
            ('No-nonsense City Cop', 'cop'),
            ('Professional Rocket League Player', 'rocket'),
            ('Lonely Guy At A Diner', 'lonely'),
            ('Pokemon Trainer', 'pokemon')
        ]
    )
    website = StringField(
        'Website',
        validators=[URL()]
    )
    birthday = DateField('Your Birthday')
    recaptcha = RecaptchaField()
    submit = SubmitField('Submit')
```
<figcaption>forms.py</figcaption>

We've added a few things here:

- **RecaptchaField** is a field type specific to Flask-WTF (hence why we import it from `flask_wtf` instead of `wtforms`). As you may expect, this allows us to add a captcha to our form to prevent bots from submitting forms. A valid Recaptcha field requires us to supply two configuration variables to work: `RECAPTCHA_PUBLIC_KEY` and `RECAPTCHA_PRIVATE_KEY`, both of which can be acquired by following the [Google API docs](https://developers.google.com/recaptcha/docs/display).
- **PasswordField** asks new users to set their passwords, which of course have hidden input on the frontend side. We leverage the `EqualTo` validator to create a second "confirm password" field, where the second field must match the first in order to be considered valid.
- **SelectField** is a dropdown menu where each "choice" is a tuple of a display name and an id.
- **URL** is a built-in validator for string fields, which ensures that the contents of a field match a URL. We're using this for the `website` field, which is our first optional field: in this case, an empty field would be accepted as valid, but a non-URL would not.
- **DateField** is our last field, which will only accept a date as input. This form is also optional.

And here's our new form:

![Handling Forms in Flask with Flask-WTF](https://res-3.cloudinary.com/hackers/image/upload/q_auto:best/v1/2019/11/flaskwtf-signup.gif)<figcaption>Our new signup form.</figcaption>

## What Happens Next?

If this were a real signup form, we'd handle user creation and database interaction here as well. As great as that sounds, I'll save your time as I know you still have an Instagram to check. Hats off to anybody who has made it through this rambling nonsense  - you deserve it.

There's only so much a human being can consume from reading a 2000-word tutorial, so we've gone ahead and posted the source for this tutorial on Github for your convenience:

{% github https://github.com/hackersandslackers/flask-wtform-tutorial %}