The End-to-End Guide to Handling Forms in Flask

Learn to create form logic and templates in Flask with the Flask-WTForms library.

The End-to-End Guide to Handling Forms in Flask

    Happy Tuesday everybody! To start this week hot, 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. Listen, if you're into creating pleasant form UI experiences, you're probably into some freaky shit. Call me.

    Flask's youth is an advantage in one regard, in that there are only so many libraries available to handle any given task. In this case, there's only one: the aptly named WTForms.

    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 to check out Instagram. Be my guest, but know this: handling form authentication, and data submission is the pinnacle of app development. This strange, ritualistic data collection affects us as people every day and is only a matter of time before your time as a software developer is consumed as well. 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.

    The Gist of it All

    Before laying down some code snippets for you to mindlessly copy+paste, it helps to understand what we're about to throw down. We're going to be jumping around between our app, so here's a look at our structure before anybody gets lost:

    ├─ /static
    │  ├─ /src
    │  │  ├─ js
    │  │  └─ less
    │  └─ /dist
    │     ├─ js
    │     ├─ css
    │     └─ img
    └─ /templates
       └─ layout.html
       └─ index.html
       └─ form.html

    At a minimum, creating a form has us working routes, form models, and templates. Here's the game plan:

    • Our form (with all its fields, labels, and potential error messages) will live in
    • will contain the logic of not only routing to the page serving the form, but also validating user input, which covers anything from error checking to session creation.
    • form.html will be the presentation layer template which will get loaded into index.html in this case. Both of those templates are wrapped by layout.html which is basically just metadata. We’ve been through this.

    Let’s start off by creating our

    What The Form

    WTForms has a nice little monopoly over handling forms for Python Frameworks ever since the early Django days. It's tried and tested, so we aren't burdened with choices here. In, we're going to import everything we need in two major chunks:

    from wtforms import Form, StringField, PasswordField, validators, SubmitField, SelectField
    from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length

    The best practice for form creation is to create a single Python class per form. Within our form class, each variable represents an input field which belongs to said form (compare this to how we create data models- not too different).

    Form input fields consist of four main parts:

    • Type of Input: Each of the things we imported directly from wtforms which contain the word "Field" represents a type of input field. StringField is a single-line text field, PasswordField allows users to input hidden passwords, etc. When we add an input to our form class, the first thing we do is specify which type of field it is.
    • Label: The human-readable name of the field, to be shown to users.
    • Validators: A validator is a restriction put on a field which 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.
    • Errors: Any time a validator is not met, we need to tell the user what went wrong. Thus, every validator has an error message.

    An in-depth breakdown of thee things can be found in WTForm's official docs.

    Creating a Form Class

    We'll get started with a standard user account signup form. Notice how we pass Form into the class below:

    from wtforms import Form, StringField, PasswordField, validators, SubmitField, SelectField
    from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Length
    class SignupForm(Form):
        """User Signup Form."""
        name = StringField('Name', [
            validators.DataRequired(message=('Don\'t be shy!'))
        email = StringField('Email', [
            Length(min=6, message=(u'Little short for an email address?')),
            Email(message=('That\'s not a valid email address.')),
            DataRequired(message=('That\'s not a valid email address.'))
        password = PasswordField('Password', validators=[
            DataRequired(message="Please enter a password."),
        confirmPassword = PasswordField('Repeat Password', validators=[
                EqualTo(password, message='Passwords must match.')
        website = StringField('Website')
        submit = SubmitField('Register')
        def validate_email(self, email):
            """Email validation."""
            user = User.query.filter_by(
            if user is not None:
                raise ValidationError('Please use a different email address.')

    Let's take a tour of this class, shall we? Our input fields are all being defined by following the same pattern:

    [VARIABLE] = [FieldType]('[LABEL]', [
            validators.[VALIDATOR TYPE](message=('[VALIDATOR ERROR'))

    Our name field is only asking that a name be provided, whereas our email field is much more strict, employing length restrictions, as well as even a built-in Email validator which verifies that the format of entered data matches an email.

    The password and confirmPassword fields are doing a bit of a dance! confirmPassword has an EqualTo validator, which means the entire form will fail if these two inputs don't match.

    Of course, let's not forget out handy submit field.

    Hardcore Validation XXX

    You'll notice the example provided contains a validate_email() function to check the database for a user record match. We can basically write any custom logic we want to validate forms this way, in case the event that the standard validators just don't cut it.

    There are a lot more validators than the ones we've covered, including a regex validator for all you elite hax0rs out there. Even if you aren't a regex guru, there are plenty of people on StackOverflow who I'm sure wouldn't mind having their patterns copied.

    Forming the Actual Form

    The form object we created in is a representation of the form we're building and it's associated logic. We've set the structure and rules for our form, but what about building the actual HTML form to correspond with it?

    Jinja's does an excellent job of tackling HTML form creation. The template we're about to create integrates seamlessly with our form class logic: the form types, labels, and associated errors all get pulled in dynamically to Jinja:

    {% block content %}
    <div class="formwrapper">
      <form method=post>
        <div class="name">
          {{'Joe Blah') }} {{ }}
          {% if %}
            <ul class="errors">{% for error in %}<li>{{ error }}</li>{% endfor %}</ul>
          {% endif %}
        <div class="email">
          {{ }} {{ }}
          {% if %}
            <ul class="errors">{% for error in %}<li>{{ error }}</li>{% endfor %}</ul>
          {% endif %}
        <div class="password">
          {{ form.password }} {{ form.password.label }}
          {% if form.password.errors %}
            <ul class="errors">{% for error in form.password.errors %}<li>{{ error }}</li>{% endfor %}</ul>
          {% endif %}
        <div class="confirm">
          {{ form.confirm }} {{ form.confirm.label }}
          {% if form.confirm.errors %}
            <ul class="errors">{% for error in form.password.errors %}<li>{{ error }}</li>{% endfor %}</ul>
          {% endif %}
        <div class="website">
          {{'') }} {{ }}
        <div class="submitbutton">
          <input id="submit" type="submit" value="Submit">
    {% for message in get_flashed_messages() %}
    <div class="alert alert-warning">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{ message }}
    {% endfor %}

    Notice how we reference fields in our form class with {{form.[FIELD]}}, and reference attributes of that field with {{form.[FIELD].[ATTRIBUTE]}}. Pulling in that form logic is as simple as accessing variables in a class with dot notation (because it is, literally).

    Not only are we pulling our fields and their labels, but our template is robust enough to handle the errors which might be triggered by the user as well! For example, check out the error block we have for the email field:

    {% if %}
         <ul class="errors">
             {% for error in %}
                 <li>{{ error }}</li>
              {% endfor %}
    {% endif %}

    We start with an if block which checks if the user has failed to submit the field, which then resulted in errors. If so, we loop through each of the errors which may have occurred (since we have 3 validators on this field), and display the errors which are relevant... all in-line to our form.

    Another way of handling errors is by utilizing Flask's flash module. A 'flash' is a temporary modal telling the user what they did wrong, which can be closed or simply expire after a number of seconds. We're using both forms of error handling here for educational purposes, but you probably won't need to most of the time.

    If it feels like you're missing something (like, how can a form have errors before it's submitted?), there's a bit of Flask magic at hand here, but take my word on this: you've actually already completed all the hard stuff! All that's left are some loose ends to tie.

    Serving Our Form

    With the pieces in place, we just need to glue them together. With haste, we make our way back to the core of our app (and where our routes view happens to live).

    Our app is a humble one with a single route, which directs users to our signup form. Our requirements are as such: if the user fails to submit a valid form, display the form again with errors. If a valid form is submitted, we ship them off to a theoretical page called dashboard.html.

    from flask import Flask, url_for, render_template, redirect, request, flash
    from forms import SignupForm
    import config
    import sys
    import json
    app = Flask(__name__, static_url_path='', static_folder="static", template_folder="templates")
    compress = FlaskStaticCompress(app)
    @app.route('/signup', methods=['GET', 'POST'])
    def signup():
        """Signup Form."""
        signup_form = SignupForm()
        if request.method == 'POST':
            if signup_form.validate():
                flash('Logged in successfully.')
                return render_template('/dashboard.html', template="dashbord-template")
        return render_template('/signup.html', form=signup_form, template="form-page")

    Our signup() route supports both GET and POST methods. Remember that "Flask magic" I mentioned earlier? Submitting a form in Flask cleverly routes the user to the page they're already on (a POST request). /signup functions both as a page and an endpoint in this regard: GET requests to /signup serve our form html, and POST requests validate said form and take further action.

    On form submission, we validate our form with signup_form.validate(), where signup_form is actually the SignupForm() class we created in And then, we... wait, that's it?! All we need to do to verify the form is a one-liner? And it checks all the user's inputs into our field?! Shouldn't this be, well, more annoying? Nope! We're coding in Python, baby: code doesn't always need to be annoying.

    With the form validated, our user is free to access the wonderful non-existent land of dashboard.html, using render_template(). We even give them a nice little message to show them we love them, with flash('Logged in successfully.').

    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.

    Todd Birchard's' avatar
    New York City Website
    Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.

    Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.