Flask HTML Forms

WTForms is a Python library that defines forms as Python classes instead of raw HTML. Flask-WTF wraps WTForms to integrate it with Flask and adds CSRF protection automatically. This approach validates data on the server and keeps form logic out of your HTML templates.

Why WTForms Over Plain HTML Forms

FeaturePlain HTML FormWTForms
ValidationManual checks in routeDeclared in the form class
CSRF protectionMust add manuallyBuilt-in automatically
ReuseCopy-paste HTMLImport the class anywhere
Error messagesWrite yourselfGenerated by validators

Installing Flask-WTF

pip install flask-wtf

Configuring a Secret Key

CSRF protection requires a secret key. Flask uses it to sign tokens. Set it in your app configuration:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-very-secret-key-here'

In production, load the secret key from an environment variable, never hardcode it in your source code.

Defining a Form Class

Create a file called forms.py in your project:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class LoginForm(FlaskForm):
    email    = StringField('Email',    validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
    submit   = SubmitField('Log In')

Each field maps to an HTML input type. Validators run automatically when the form is submitted.

Common Field Types

WTForms FieldHTML Equivalent
StringField<input type="text">
PasswordField<input type="password">
EmailField<input type="email">
TextAreaField<textarea>
BooleanField<input type="checkbox">
SelectField<select>
IntegerField<input type="number">
SubmitField<button type="submit">

Using the Form in a Route

from flask import Flask, render_template, redirect, url_for
from forms import LoginForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        email    = form.email.data
        password = form.password.data
        # authenticate user...
        return redirect(url_for('dashboard'))
    return render_template('login.html', form=form)

form.validate_on_submit() returns True only when the request is POST and all validators pass. On GET requests, it returns False and the form displays. On a failed POST (invalid data), it also returns False and the form redisplays with error messages.

Rendering the Form in a Template

Create templates/login.html:

<h1>Login</h1>
<form method="POST">
  {{ form.hidden_tag() }}

  <div>
    {{ form.email.label }}
    {{ form.email(placeholder="you@example.com") }}
    {% for error in form.email.errors %}
      <span style="color:red">{{ error }}</span>
    {% endfor %}
  </div>

  <div>
    {{ form.password.label }}
    {{ form.password() }}
    {% for error in form.password.errors %}
      <span style="color:red">{{ error }}</span>
    {% endfor %}
  </div>

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

{{ form.hidden_tag() }} inserts the CSRF token as a hidden input. This token verifies that the form submission came from your own site, not from an attacker's page.

What CSRF Protection Does

Without CSRF:
  Attacker's page ──POST /transfer?amount=1000──▶ Your bank

With CSRF token:
  Attacker's page ──POST (no valid token)──▶ Flask rejects: 400 Bad Request
  Your own page   ──POST (valid token)────▶ Flask accepts: processes form

Summary

Flask-WTF brings form classes, automatic validation, and CSRF protection to Flask. Define form fields and validators in a Python class. Pass a form instance to render_template(). Call form.validate_on_submit() in the route to handle valid submissions. Render each field and its errors in the template using Jinja2. This approach scales better than manual HTML forms as your application grows.

Leave a Comment

Your email address will not be published. Required fields are marked *