Flask Form Validation with Flask-WTF

Introduction

Form validation is one of the most essential components of data entry in web applications. Users can make mistakes, some users are malicious. With input validation, we protect our app from bad data that affects business logic and malicious input meant to harm our systems

Trying to process unvalidated user inputs can cause unexpected/unhandled bugs, if not a server crash. In this context, validating data means verifying input and checking if it meets certain expectations or criteria(s). Data validation can be done on both the front and back end.

In this tutorial, we will learn how to validate user input in Flask forms using the Flask-WTForms extension.

By the end of this tutorial, we will have the following user registration form with validation criteria:

Registration form with validation

We will use Flask version 1.1.2 and Flask-WTF with version 0.14.3.

Setup

While not necessary, we recommend you create a virtual environment to follow along:

$ mkdir flask-form-validation
$ cd flask-form-validation
$ python3 -m venv .
$ . bin/activate

In your activated virtual environment, we will install our packages by typing:

$ pip install Flask Flask-WTF

Note that if you want to use email validation, you'll also need to install the email_validator package (current version is 1.1.1):

$ pip3 install email_validator

Now let's create our necessary files. We'll start by creating a basic app.py, which, for simplicity, will contain our Flask app, routes, and forms:

from flask import Flask, render_template

app = Flask(__name__, template_folder='.')
app.config['SECRET_KEY']='LongAndRandomSecretKey'

We created a Flask object and set template_folder to the current folder. We then assigned the Flask object into app variable. We added SECRET_KEY to our app object's configuration.

The SECRET_KEY is commonly used for encryption with database connections and browser sessions. WTForms will use the SECRET_KEY as a salt to create a CSRF token. You can read more about CSRF on this wiki page.

If your application already uses the SECRET_KEY config for other purposes, you would want to create a new one for WTForms. In that case, you can set the WTF_CSRF_SECRET_KEY config.

Let's create and add a form to our current app.py:

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'))
    submit = SubmitField(label=('Submit'))

# ...

Our simple GreetUserForm class contains a StringField. As the name implies, this field expects and will return a string value (you can always convert that input to other data types as the need arises). The name of the field is username, and we'll use this name to access data of the form element.

The label paremeters are what will be rendered on our page so that users would understand what data a form element captures. We also have a submit button, which will try to submit the form if all fields pass our validation criteria.

Now that we're set up, let's use WTForms to validate our data!

Flask Form Validation With Flask-WTForms

Let's begin by creating a route to display and process our form:

# ...

@app.route('/', methods=('GET', 'POST'))
def index():
    form = GreetUserForm()
    if form.validate_on_submit():
        return f'''<h1> Welcome {form.username.data} </h1>'''
    return render_template('index.html', form=form)

Our route has GET and POST methods. The GET method displays the form, whereas the POST method processes the form data on submission. We set the URL path to /, or the root URL, so it will appear as our web app's home page. We render the index.html template and pass the form object as a parameter.

Let's pause and pay close attention to this line: if form.validate_on_submit():. This rule says 'if the request method is POST and if the form field(s) are valid, then proceed. If our form input passes our validation criteria, on the next page a simple greet message will be rendered with the user's name. Notice here we used field name (username) to access input data.

To see the form, we need to create the index.html template. Create the file and add the following code to it:

<form method="POST" action="">
    <div class="form-row">
        <div class="form-group col-md-6">
            {{ form.csrf_token() }}
            <label for=""> {{ form.username.label }}</label>
            {{ form.username }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </div>
</form>

We use our form object to pass WTform elements into Jinja2 - the template parser for Flask.

Note: The csrf_token is generated automatically by the WTForms and it changes each time the page is rendered. This helps us to protect our site against CSRF attacks. By default, it is a hidden field. You could also choose to use {{ form.hidden_field() }} to render all hidden fields, including CSRF token, but that's not advised.

Now, let’s go to our terminal to start our Flask app by typing:

$ FLASK_ENV=development flask run

For convenience, we set the FLASK_ENV environment variable to 'development' while developing. This allows the app to hot-reload each time we hit save. For Windows you may have to use set FLASK_ENV=development into your terminal/console before running your flask app.

Here's what we'll see if we navigate to the localhost:

Running Flask app with form

Type a name in the input field and submit the form. You will see the greetings message we defined in our route:

Successful form submission with username

It works as expected. But what if we didn't type anything into the input field? It would still validate the form:

Successful form submission without name

Let's prevent that from happening and only allow users who typed their names to see the next page. To do so, we need to ensure that our username field has input data.

We'll import one of the built-in WTForms validation methods: DataRequired() from wtforms.validators and pass it into our username field.

# ...
from wtforms.validators import ValidationError, DataRequired

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
                           validators=[DataRequired()])
    submit = SubmitField(label=('Submit'))

# ...

Notice that we are passing the validators parameter as a list. This tells us that we can have multiple validators for each field.

Now that we are using DataRequired(), the username field will not be validated if there is no input data:

Tooltip displayed to user when form data is blank

In fact, if we right-click and inspect the form element, we'll see that WTForms automatically added the required attribute to the input field:

Inspecting the HTML of the form to see the "required" attribute of the input field

By doing so, WTForms adds a basic front-end validation to our form field. You wouldn't be able to submit that form without the username field even if you try to post the form using tools like cURL or Postman.

Now, let's say we want to set a new validation rule that will only allow names that are at least 5 characters long. We can use the Length() validator with min parameter:

# ...
from wtforms.validators import ValidationError, DataRequired, Length

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'), 
    	validators=[DataRequired(), Length(min=5)])
    submit = SubmitField(label=('Submit'))

# ...

If we try to submit the form with input data less than 5 chars long, the validation criteria will not be fulfilled, and the submission will fail:

wtforms validation fail

Clicking on the submit button does nothing for invalid data, it also does not display any error to the user. We need to provide error messages so the user would understand what's going on and how to fix it.

In our index.html template, right under the {{ form.username }}, add the following Jinja2 for-loop to display errors:

 {% for field, errors in form.errors.items() %}
    <small class="form-text text-muted ">
        {{ ', '.join(errors) }}
    </small>
{% endfor %}

Our form can render clean validation errors now:

Form rendering errors for a short username

For any reason, if we need to limit the maximum length of our field data, we can do it by passing the max parameter to the Length() validator. It's also possible to customize the error message by passing an optional message parameter with a custom error string.

Let's update the username field accordingly:

# ...

class GreetUserForm(FlaskForm):
    username = StringField(label=('Enter Your Name:'),
        validators=[DataRequired(), 
        Length(min=5, max=64, message='Name length must be between %(min)d and %(max)dcharacters') ])
    submit = SubmitField(label=('Submit'))

# ...

Form rendering errors for a short username with a custom message

More WTForms Fields and Validators With the User Registration Form

Our current form has a single field, which is kind of dull. WTForms provides extensive form validation criteria and a variety of form fields, so let's take advantage of it and create something with practical use.

We'll create a user registration form and use built-in WTForms validators.

We will use the DataRequired() validator for the fields that we want to make sure that the user fills in. We'll check the minimum and maximum length of the fields with Length() validator, validate emails with Email() validator and check if two fields contain the same data with EqualTo() validator.

Remove the GreetUserForm class and replace the beginning of your code with our new form:

from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, \
    SubmitField
from wtforms.validators import ValidationError, DataRequired, \
    Email, EqualTo, Length

class CreateUserForm(FlaskForm):
    username = StringField(label=('Username'), 
        validators=[DataRequired(), 
        Length(max=64)])
    email = StringField(label=('Email'), 
        validators=[DataRequired(), 
        Email(), 
        Length(max=120)])
    password = PasswordField(label=('Password'), 
        validators=[DataRequired(), 
        Length(min=8, message='Password should be at least %(min)d characters long')])
    confirm_password = PasswordField(
        label=('Confirm Password'), 
        validators=[DataRequired(message='*Required'),
        EqualTo('password', message='Both password fields must be equal!')])

    receive_emails = BooleanField(label=('Receive merketting emails.'))

    submit = SubmitField(label=('Submit'))

# ...    

We have four different fields in our forms. The last one is a regular submit button. We used StringField to get string input from users, such as username and email. On the other hand, PasswordField hides the password text on the front-end. BooleanField renders as a checkbox on the front-end since it only contains either True (Checked) or False (Unchecked) values.

We need to modify out index.html template to render our new form fields:

<div class="container">
    <h2>Registration Form</h2>
    {% for field, errors in form.errors.items() %}
    {{ ', '.join(errors) }}
    {% endfor %}
    <form class="form-horizontal" method="POST" action="">
        {{ form.csrf_token() }}
        <div class="form-group">
            {{ form.username.label }}
            {{ form.username(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.email.label }}
            {{ form.email(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.password.label }}
            {{ form.password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.confirm_password.label }}
            {{ form.confirm_password(class="form-control") }}
        </div>
        <div class="form-group">
            {{ form.receive_emails.label }}
        </div>
        <div class="form-group">
            {{ form.submit(class="btn btn-primary")}}
        </div>
    </form>
</div>

Our form fields rendered properly as you can see:

User registration form rendering correctly

Note: If your website is going to have multiple different forms, you may want to use Jinja2 macros instead of typing each form field one by one. Using macros is beyond the scope of this article, but it greatly speeds up the form creation processes.

Creating Your Own Custom Validators

In most web sites, certain characters are not allowed in usernames. It can be for security purposes, it can be for cosmetics. WTForms doesn't have that logic by default but we can define it ourselves.

WTForms allows us to add custom validators by adding a validation method to our UserRegistrationForm class. Let's implement that custom validation into our form by adding the validate_username() method right below the submit button.

# ...

class UserRegistrationForm(FlaskForm):
    # ...
    submit = SubmitField(label=('Submit'))

    def validate_username(self, username):
        excluded_chars = " *?!'^+%&/()=}][{$#"
        for char in self.username.data:
            if char in excluded_chars:
                raise ValidationError(
                    f"Character {char} is not allowed in username.")
                
# ...

We can add as many or as few validation methods as we like. WTForms will run validation methods automatically once defined.

The ValidationError class gives us a convenient way to define our custom validation message. Note that you will need to import it from wtforms.validators before using it.

Let's test this new method by entering proper data to all fields except the username field, which will contain an excluded character - '%'.

User form failing to validate because of our custom validation

As you can see, our custom validation method runs perfectly and provides us with a clean validation error, which helps us to understand what's wrong with our input data. Doing so, greatly improves the user experience.

You can use external libraries, your database, or APIs to combine with WTForms and to validate the incoming input data. When you want to capture {{ form.some_field.data }} and write into or query from the database, use WTForms validators to ensure it's safe to be saved.

Note: We've excluded most of the HTML codes out since they are not directly related to our tutorial. The full code will be available on this GitHub repository, in case you want to check out.

Conclusion

Validating data is one of the most essential parts of the Flask web applications. Flask-WTforms provides very powerful and easy to learn ways to handle form data.

Now that you know the fundamentals of data validation with Flask-WTF, you can go ahead and apply your own validation logic and/or implement your own methods for both security and better user experience.

Author image
Full-stack software developer. Python & C#. Linux user. Excel Ninja