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/un-handled bugs, if not a server crash. In this context, validating data means verifying input and checking if it meets certain expectations or criteria. 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:
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 to the 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
parameters 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 the 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:
Type a name in the input field and submit the form. You will see the greetings message we defined in our route:
It works as expected. But what if we didn't type anything into the input field? It would still validate the form:
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.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
# ...
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:
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:
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:
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:
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'))
# ...
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 marketing 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:
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 - '%'.
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.