Forms with Flask-WTF

Bookshelf needs a way for a user to add a new book: a title, an author, a page count, a reading status. You could build that as plain HTML and pull values out of request.form by hand — but Flask, true to its microframework philosophy, ships no forms library of its own. Unlike Django, which bundles a forms system into the core framework, Flask leaves this entirely up to you to choose. The overwhelming choice the community has settled on is Flask-WTF.

Why Flask-WTF

Flask-WTF is a thin, Flask-friendly wrapper around WTForms, a mature, framework-agnostic Python forms library. WTForms alone can define fields and validators; Flask-WTF adds the glue that makes it feel native inside a Flask app — automatic CSRF protection (covered fully in the next lesson), easy integration with Flask's request object, and file-upload support. In practice, "using WTForms in Flask" and "using Flask-WTF" mean the same thing — it's the standard, and there's little reason to reach for anything else for server-rendered forms.

Installing it is a single pip command:

pip install flask-wtf

Defining a Form Class

A Flask-WTF form is a plain Python class that inherits from FlaskForm, with each field declared as a class attribute. Here's Bookshelf's form for adding a new book:

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, SubmitField
from wtforms.validators import DataRequired

class AddBookForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired()])
    pages = IntegerField('Pages')
    submit = SubmitField('Add Book')

Each field is given a label string ('Title', 'Pages') — that's the human-readable text shown next to the input when the field renders — and, optionally, a list of validators that will run when the form is submitted. DataRequired() is the most basic one: it rejects an empty submission. The full validators picture, including custom per-field validation, is the subject of the next lesson, Form Validation & CSRF.

Field Types: A Reference

WTForms ships a set of field classes covering the common HTML input types. These are the ones Bookshelf's forms lean on most:

Field classRenders asBookshelf usage
StringField<input type="text">A book's title, an author's name
IntegerField<input type="text">, coerced to intA book's pages
SelectField<select> dropdownA book's status — want_to_read / reading / finished
BooleanField<input type="checkbox">e.g. "Mark as a favorite"
SubmitField<input type="submit">The form's submit button

The SelectField needs a bit more than the others — it must be told what options to offer, via a choices argument of (value, label) tuples:

from wtforms import SelectField, BooleanField

class AddBookForm(FlaskForm):
    # ...title, pages, submit as above...
    status = SelectField(
        'Status',
        choices=[
            ('want_to_read', 'Want to Read'),
            ('reading', 'Currently Reading'),
            ('finished', 'Finished'),
        ],
    )
    is_favorite = BooleanField('Mark as favorite')

Rendering a Form in a Template

A form instance is passed to a template like any other variable, and each field knows how to render its own label and its own input tag. Calling the field — form.title() — produces the actual <input> HTML; accessing form.title.label produces the matching <label>. Here's the full pattern for Bookshelf's add-book form:

<!-- templates/add_book.html -->
{% extends "base.html" %}
{% block content %}
<form method="POST">
    {{ form.hidden_tag() }}

    {{ form.title.label }} {{ form.title() }}
    {{ form.pages.label }} {{ form.pages() }}
    {{ form.status.label }} {{ form.status() }}
    {{ form.is_favorite.label }} {{ form.is_favorite() }}

    {{ form.submit() }}
</form>
{% endblock %}

{{ form.hidden_tag() }} is the one line in this template that has nothing to do with any field you defined — it renders a hidden CSRF-protection token that Flask-WTF adds to every FlaskForm automatically. We'll unpack exactly what that token protects against, and what happens without it, in the next lesson, Form Validation & CSRF.

Note: All of this is correct, real Flask-WTF usage — but this website's own project doesn't run a Flask server, so you won't see this form actually render or submit on this page.

Handling Submission in a View

The standard Flask-WTF pattern is the same one view function handling both cases of a form's life: a plain GET request that should show an empty form, and a POST request carrying submitted data that needs validating and saving. The shortcut that makes this genuinely convenient is form.validate_on_submit() — a single method call that checks both "was this request a POST?" and "did every validator pass?" in one go. If either check fails, it returns False and the view falls through to re-rendering the form (with error messages already attached, as the next lesson covers).

# app.py
from flask import Flask, render_template, redirect, url_for
from forms import AddBookForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'change-this-in-production'

@app.route('/books/add', methods=['GET', 'POST'])
def add_book():
    form = AddBookForm()

    if form.validate_on_submit():
        # GET requests never reach here; only a valid POST does
        book = Book(
            title=form.title.data,
            pages=form.pages.data,
            status=form.status.data,
            user_id=current_user_id(),
        )
        db.session.add(book)
        db.session.commit()
        return redirect(url_for('book_list'))

    # GET request, or a POST that failed validation: show the (possibly re-populated) form
    return render_template('add_book.html', form=form)

Notice what form.validate_on_submit() saves you from writing: no manual if request.method == 'POST': check, no manual loop calling each validator, no manual check of a returned boolean. Once validation succeeds, each field's cleaned, correctly typed value is available as form.<fieldname>.data — form.title.data is a Python str, form.pages.data is already a Python int (WTForms performed that conversion for you as part of validation), ready to hand straight to a new Book row.

Coming up next: Form Validation & CSRF covers WTForms validators in depth, custom per-field validation, displaying error messages in a template, and exactly how that hidden_tag() CSRF token protects Bookshelf.