Form Handling — Flask-WTF
In this chapter, you will learn to use Flask-WTF to standardize form handling: defining form classes, automatic CSRF protection, field validation, and error messages.
Why use Flask-WTF?
The registration and login forms in the previous chapter were handwritten: manually fromrequest.form.get()Getting values, manual validation, manually displaying errors.
Flask-WTFA wrapper around WTForms for Flask, providing:
- Form class definition, centralized management of fields + validation rules.
- Automatic CSRF Protection
- Rendering Fields and Error Messages in Templates
form.validate_on_submit()Unified handling of GET/POST determination and validation.
Installation and Configuration
(venv) $ pip install flask-wtf
Configure SECRET_KEY in app.py (CSRF Token depends on this key):
Example
app.config['SECRET_KEY'] = 'your-secret-key' # Use environment variables for production environment
app.config['WTF_CSRF_ENABLED'] = True # Enabled by default, explicitly declare it
Define form class
Example
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, EmailField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
from app.models import User
class RegisterForm(FlaskForm):
Registration Form
username = StringField(Username, validators=[
DataRequired(message=Username cannot be empty),
Length(min=3, max=50, message=Username must be 3-50 characters)
])
email = EmailField(Email, validators=[
DataRequired(message=Email cannot be empty),
Email(message=Please enter a valid email address)
])
password = PasswordField(Password, validators=[
DataRequired(message=Password cannot be empty),
Length(min=6, message=Password must be at least 6 characters)
])
confirm_password = PasswordField(Confirm password, validators=[
DataRequired(message=Please enter the password again),
EqualTo('password', message=The two passwords do not match)
])
# Custom validator: check whether the username is already registered
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError(This username is already taken.)
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError(This email is already registered.)
class LoginForm(FlaskForm):
"""Login form"""
username = StringField(Username, validators=[
DataRequired(message=Username cannot be empty)
])
password = PasswordField(Password, validators=[
DataRequired(message=Password cannot be empty)
])
remember = BooleanField(Remember me)
Common field types
| Field class | HTML tag | Description |
|---|---|---|
| StringField | <input type="text"> | Single-line text |
| PasswordField | <input type="password"> | Password input |
| EmailField | <input type="email"> | Email input |
| TextAreaField | <textarea> | Multi-line text |
| BooleanField | <input type="checkbox"> | Checkbox |
| SelectField | <select> | Dropdown selection |
Common validators
| Validator | Function |
|---|---|
| DataRequired() | Field cannot be empty |
| Length(min=, max=) | Character length limit |
| Email() | Email format validation |
| EqualTo('field_name') | Matches the value of another field |
| Optional() | Allow field to be empty (skip subsequent validators). |
Using Forms in View Functions
Example
from flask import Blueprint, render_template, redirect, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from app.forms import RegisterForm, LoginForm
from app.models import User, db
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/register", methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegisterForm()
# form.validate_on_submit() validates the form on POST requests
# Returns False on GET requests or when validation fails
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
login_user(user)
flash(fRegistration successful, welcome, {user.username}!, 'success')
return redirect(url_for('main.index'))
# On GET requests or validation failure, render the template with the form
return render_template('register.html', form=form)
@auth_bp.route("/login", methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash(Incorrect username or password., 'error')
return render_template('login.html', form=form)
login_user(user, remember=form.remember.data)
flash(fWelcome back, {user.username}!, 'success')
next_page = request.args.get('next')
return redirect(next_page or url_for('main.index'))
return render_template('login.html', form=form)
Rendering forms in templates
Example
{% extends 'base.html' %}
{% block title %}Register - EXAMPLE Blog{% endblock %}
{% block content %}
<div class="auth-form">
<h2>Register</h2>
<form method="post">
<!-- form.hidden_tag() automatically generates the CSRF Token hidden field -->
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-input") }}
{% if form.username.errors %}
{% for error in form.username.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-input") }}
{% for error in form.email.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-input") }}
{% for error in form.password.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
<div class="form-group">
{{ form.confirm_password.label }}
{{ form.confirm_password(class="form-input") }}
{% for error in form.confirm_password.errors %}
<span class="field-error">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn-submit") }}
</form>
<p class="form-footer">
Already have an account?<a href="{{ url_for('auth.login') }}">Login now</a>
</p>
</div>
{% endblock %}
Flask-WTF's CSRF protection is automatic:
form.hidden_tag()Will generate a hidden<input>Contains a random token; the server validates this token before accepting the POST request. Any form submitted from an external site is rejected because it lacks the correct token.
Manual request.form vs Flask-WTF comparison
| Aspect | Manual request.form | Flask-WTF |
|---|---|---|
| Lines of code | For each field, repeat the three steps: retrieve/validate/report | Define form class once |
| CSRF protection | Manual implementation required | Automatic |
| Custom validation | Manual if/else | validate_xxx method |
| Error display | Manual concatenation | form.field.errors auto-collection |
| Applicable scenarios | Simple forms (e.g., search box) | Complex Forms (Registration, Login, Profile Editing) |
Chapter summary
In this chapter, you refactored the authentication form using Flask-WTF: FlaskForm defines fields and validators, validate_xxx custom validation, form.validate_on_submit() handles POST validation uniformly, form.hidden_tag() provides automatic CSRF protection, and error messages are rendered in templates.
Form handling is now more standardized and secure.
other extensions