Forms and Flask-WTF

Introduction

HTML forms send user input to your server via POST. Reading request.form works for simple cases, but production apps need validation, error messages, and CSRF protection. Flask-WTF integrates WTForms with Flask sessions so you build secure forms with less boilerplate.

Prerequisites

Manual Form Handling

Code explanation:

  • You validate every field yourself and re-render the template on failure
  • Works for learning—gets repetitive as forms grow

templates/contact.html (simplified):

html
<form method="post">
  <input name="name" value="{{ name }}">
  <input name="email" type="email">
  <button type="submit">Send</button>
  {% for error in errors %}
    <p class="error">{{ error }}</p>
  {% endfor %}
</form>

Install Flask-WTF

bash
pip install Flask-WTF

Add to requirements.txt:

text
Flask-WTF==1.2.1
WTForms==3.1.2

Define a Form Class

forms.py:

Code explanation:

  • FlaskForm subclasses Form and adds CSRF integration
  • validators run on the server when the form is submitted

View With validate_on_submit

Code explanation:

  • validate_on_submit() is True only on POST when all validators pass
  • form.name.data reads the cleaned field value

Template With CSRF Token

templates/contact_wtf.html:

Code explanation:

  • form.hidden_tag() renders the CSRF token field—required on every POST form
  • Field-level form.name.errors lists validation messages

Warning

CSRF Token Missing

If you omit hidden_tag(), Flask-WTF rejects POST with 400 Bad Request or CSRF error.

CSRF Configuration

python
app.config["WTF_CSRF_ENABLED"] = True
app.config["WTF_CSRF_TIME_LIMIT"] = None

Disable only in tests with WTF_CSRF_ENABLED = False in TestingConfig—never in production.

Custom Validators

python
from wtforms import ValidationError
 
def no_spam(form, field):
    if "http://" in field.data.lower():
        raise ValidationError("Links are not allowed in messages.")
 
class ContactForm(FlaskForm):
    message = TextAreaField(
        "Message",
        validators=[DataRequired(), no_spam],
    )

Select and Boolean Fields

python
from wtforms import SelectField, BooleanField
 
class SettingsForm(FlaskForm):
    theme = SelectField(
        "Theme",
        choices=[("light", "Light"), ("dark", "Dark")],
    )
    newsletter = BooleanField("Subscribe to newsletter")
    submit = SubmitField("Save")

FAQ

CSRF token missing error?

Ensure SECRET_KEY is set and template includes {{ form.hidden_tag() }}.

Form validates on GET?

Use validate_on_submit(), not form.validate() alone on GET.

Repopulate fields after error?

Flask-WTF keeps submitted data in form.field.data automatically when validation fails.

AJAX JSON forms?

Flask-WTF targets HTML forms—APIs use JSON validation libraries instead.

Multiple forms on one page?

Pass form=MainForm() and use separate endpoints or prefix on fields.

File upload with WTF?

Use FileField—see file uploads chapter.