Build HTML Forms in a Flask App With Python and WTForms

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To set up the project on your local machine, please follow the directions provided in the README.md file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.

Using WTForms

As we've seen, manually writing out logic for rendering the form and validations manually for each form in a complex SaaS application would quickly become tedious. WTForms is a Python library that helps us handle rendering form inputs, validation, editing data, and CSRF protection.

In the last chapter we used SQLAlchemy to provide us with a more "Python-ic" way to manage our database schema and querying. WTForms similarly provides a "Python-ic" abstraction for forms and form fields.

We're going to go back and convert our "New Product" form to use WTForms.

To start with WTForms, add the Flask extension for WTForms Flask-WTF your requirements.txt file and then install it within your virtual environment. This will also install the actual WTForms library as well.

(env) $ pip install -r requirements.txt

Then create a file in the yumroad folder for us to store our form definitions called forms.py. This file will be similar to models.py in that it's a collection of classes where we declare the fields we want in our forms or models. You might see this pattern referred to as declarative class definitions.

If you see an error message about needing a secret key, you may need to add WTF_CSRF_ENABLED = False to your BaseConfig in yumroad/config.py depending on which version of Flask you install. We will enable CSRF later on in this chapter

Creating a product with a form

Our product catalog needs a way for our users to create products, and the two fields we have right now are one for the name and description, so our form should also contain those two fields.

Our first step to creating a form will be to create a class to define the form. In our models file, the classes inherit from a base model from SQLAlchemy (db.Model). Our form classes will similarly inherit from a base class named FlaskForm provided by flask_wtf.

from flask_wtf import FlaskForm

class ProductForm(FlaskForm):
    # our fields will be declared here
    pass

The next step is to declare the fields that this form will use. WTForms defines a set of fields for us to use, here are some of the common fields. A full list is available on the WTForms documentation.

NameTypeRendered As
StringFieldStringtext input
TextAreaFieldStringtextarea input
DateFielddatetime.datetimeA text input for a formatted date string
BooleanFieldBooleanA checkbox input
SelectFieldStringA select input
FileFieldDataA file upload field

We know that both of the fields we'll want our users to input are strings, but since the product description will be longer than the name, we want to render a textarea input for the description and a text input for the name.

To declare a field, we will import the field from wtforms.field and then initialize them with the name of the field that we'd like to display to users.

from flask_wtf import FlaskForm

from wtforms.fields import StringField, SubmitField

class ProductForm(FlaskForm):
    name = StringField('Name')
    description = StringField('Description')
    submit = SubmitField('Create Product')

This lesson preview is part of the Fullstack Flask: Build a Complete SaaS App with Flask course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.

Thumbnail for the \newline course Fullstack Flask: Build a Complete SaaS App with Flask
  • [00:00 - 00:26] As we've just seen, writing out the logic for rendering the form and creating validations for every single form we create in our complex SAS application can quickly become tedious. WT forms the Python library that can help us render form inputs to data validation, help us edit data, and also provide cross-site request forgery protection.

    [00:27 - 00:43] In the last section of this course, we seek while coming to provide us with the more Pythonic way of managing our database schema and querying the database. WT forms similarly provides a Pythonic way for us to work with forms and form data.

    [00:44 - 00:57] We're going to go back and convert our new product form to use WT forms. First off, we're going to add WT forms and Flask extension for WT forms to our requirements.

    [00:58 - 01:10] The way we're going to do that is by installing a library known as Flask-WTF as an extension. Then we're going to go back into our terminal and we're going to install that.

    [01:11 - 01:27] To do that, we're going to want to pip install the char requirements.txt just like usual. That installs both Flask-WTF and WT forms.

    [01:28 - 01:41] Going back in our codebase, we can go ahead and create a forms.py file somewhat conceptually similar to how we created models.py. The way we're going to define forms is also through classes.

    [01:42 - 01:59] The way we're going to do this is we're going to import a base form called Flask form from Flask-WTF. This is similar to our database model base class.

    [02:00 - 02:26] When we define a class, for example, a new product form, it's going to inherit from the base class and we're going to define some things here. Prior to how we declare models here by setting the name and then the type by using SQLKME and WT forms, we're going to define the name of a field and then the field type here.

    [02:27 - 02:40] The field type is going to come from something that WT forms recognizes. WT forms provides us with a few types of fields that we can import, namely string fields, exterior fields, among others.

    [02:41 - 02:56] Our job when we go back into our code is to go ahead and import those. We're going to say from WT forms.fields import string field and submit field.

    [02:57 - 03:05] What we're going to do is we're going to say the name field is a string field and its name is going to be name. That's going to be the label that we're going to use.

    [03:06 - 03:22] Description is equal to string field, description, and lastly we're going to have a submit field. We're going to call that create perfect.

    [03:23 - 03:36] At this point, we now have a form that we've declared with WT forms. In addition to specifying fields, we can also specify validations on the fields .

    [03:37 - 03:50] The validations are things we can add to fields that help WT forms figure out whether the data that a user submitted is actually valid. We can actually go ahead and configure those right now.

    [03:51 - 04:00] Similar to how we imported fields, we're going to import these validations. From WT forms validators import length.

    [04:01 - 04:19] Specifically what we want to import is a length requirement of a least character three characters. What we can do is say something like length is min4 and the max is 60 characters.

    [04:20 - 04:31] If we had other validations we wanted to add, we can just add them to that list here. Now we've defined our form along with the requirements to tell if a form is valid.

    [04:32 - 04:51] To display our form, we're going to have to import it within the products blueprint, which we can do here from yumroad.forms.import.product form. Then our job is going to be to pass this to the template as well.

    [04:52 - 04:55] First off is we're going to initialize the form. We're going to call product form.

    [04:56 - 05:04] Then we're going to also have to pass it to the template. Here we want to pass the form is equal to the form.

    [05:05 - 05:11] Next up, we don't just want to check that this was a post request. We also want to check that the user's input was valid.

    [05:12 - 05:19] WT forms provides us with the easy way to check for both. That it's a post request as well as the input of its valid.

    [05:20 - 05:30] That comes through a method called validate on submit. If it is a post request and the data is valid, it's going to let us do this logic.

    [05:31 - 05:41] Otherwise it's going to render the template with the form. Now we go into the template and we edit how we render the form.

    [05:42 - 05:59] Here's our form right now. Instead of rendering all of this logic, what we can do is we can go ahead and for example, simply do form.name.label as well as form.name.

    [06:00 - 06:15] This will render both the label as well as the name for each field. Then similarly we can do form.description.label and similarly run form.

    [06:16 - 06:30] description as well. Then lastly, we'll want to run the submit tag. One thing we'll also want to do is we won't want to use the data directly from request.forum.

    [06:31 - 06:47] We'll want WT forms to take a crack at it first. The reason that WT forms should take a crack at it first is because when we specify the types here, for example, if we specify it as an integer, it may not always be an integer in the HTTP request.

    [06:48 - 07:02] WT forms might be doing some type conversion on the backend to convert it to an object that we can deal with in Python. For example, a date time object might come over HTTP as a string.

    [07:03 - 07:20] When it comes to Python, we'd really like to interact with it as if it was an actual date time object. Instead of directly parsing the request.form value, which will always be a string of some kind, what we can do is we can use the data directly from WT forms.

    [07:21 - 07:34] Form.name.data and form.description.data. And when we load our form, we're probably going to get an error here.

    [07:35 - 07:54] And the reason we get an error is that there is a configuration variable we need to set and by default, plus WT forms assumes that we have turned on cross-site request forgery protection. So for now, we're going to change that configuration variable to be false.

    [07:55 - 08:04] We need to disable these two fields in both dev and test. By default, currently, WT forms turns this on.

    [08:05 - 08:19] Don't worry, we'll be turning them back on in a little bit once we've covered CSRF protection. Now that we've turned it off, if we load our browser, we can see that our form properly renders and we can submit it.

    [08:20 - 08:29] So if I submit something invalid that looks like this, it's not going to actually create the record in our database. But it's also not showing our error messages.

    [08:30 - 08:38] So let's fix that. So one thing we can do to avoid a lot of the repetition here is creating this macro.

    [08:39 - 08:52] And so I've created a file called formalpers.html here and I've started a macro called render field. So the idea here is that we're going to write a lot of logic here to properly render our forms.

    [08:53 - 09:26] So we could add some styling here by using bootstrap's form group class. And within there, we can add a label and the label will be for the field.name and it will render the field label value, then we want to render the actual input.

    [09:27 - 09:36] The way we're going to render the input is we're going to call field, which is a function. Now in order to pass in classes to the field, we're going to have to pass in something here.

    [09:37 - 09:54] So if we just wanted to pass in form control as a class to the field, we could do that here. Now there's another thing I want to do, which is I want to pass in a CSS class called is invalid if the form is invalid.

    [09:55 - 10:00] And to do that, we can use a little bit of ginger. So do that.

    [10:01 - 10:19] What I'm going to do is I'm going to make this a list. I'm going to use a ternary statement here so that it only shows is invalid if fields.error is a truthy value.

    [10:20 - 10:32] Then we're also going to pass any keyword arguments that we get to render field as well. Now what we're going to do is import that macro we defined from form helpers.

    [10:33 - 10:44] html and then go ahead and use that in our template. First dot HTML import lender field.

    [10:45 - 11:05] And then here what we can do is we can just do render field and then we can take form.name and then we can do form.description. Then as the last thing, we can either render the submit tag like before or we can just write our own submit tag.

    [11:06 - 11:23] So here you can see it's possible to mix our own HTML form elements along with WTForm as well. Okay, now that we're creating the submit form within here, there's really no reason why we need to have this submit field here.

    [11:24 - 11:37] So let's go and get rid of that. We've also done here is added some way for our errors to be rendered on the page by adding the invalid feedback class to our list of errors.

    [11:38 - 11:51] We're also making sure that we accept any additional keyword arguments provided to the macro and passing them back into field. All right, now looking at our browser, we can see that everything works just as before.

    [11:52 - 12:11] But if I enter something invalid here, like a name that's too short, it's going to go and render an actual error message for me, which is great. Another thing that's really nice is that the forms here look a lot better, which is a good start.

    [12:12 - 12:19] In the next video, we're going to talk about how to implement editing and then also how to enable cross-site request forgery protection.