YouTip LogoYouTip

Flask Blog Wtf

In this chapter, you will learn how to use Flask-WTF to handle forms in a standardized way: defining form classes, automatic CSRF protection, field validation, and error display.


Why Use Flask-WTF?

In the previous chapter, the registration and login forms were handwritten: manually retrieving values from request.form.get(), manually validating, and manually displaying errors.

Flask-WTF is built on WTForms and packaged for Flask, providing:

  • Form class definitions with centralized management of fields and validation rules
  • Automatic CSRF protection
  • Rendering fields and error messages in templates
  • form.validate_on_submit() to uniformly handle GET/POST logic and validation

Installation and Configuration

(venv) $ pip install flask-wtf

Configure SECRET_KEY in app.py (CSRF Token depends on this secret key):

Example

# File path: app.py

 app.config['SECRET_KEY']='your-secret-key'# Use environment variables in production

 app.config['WTF_CSRF_ENABLED']=True# Enabled by default, explicitly declared here

Defining Form Classes

Example

# File path: app/forms.py (new file)

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 is required'),

 Length(min=3,max=50, message='Username must be 3-50 characters')

])

email= EmailField('Email', validators=[

 DataRequired(message='Email is required'),

 Email(message='Please enter a valid email address')

])

 password = PasswordField('Password', validators=[

 DataRequired(message='Password is required'),

 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='Passwords do not match')

])

# Custom validator: check if 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 is required')

])

 password = PasswordField('Password', validators=[

 DataRequired(message='Password is required')

])

 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 select

Common Validators

Validator Function
DataRequired() Field cannot be empty
Length(min=, max=) Character length limit
Email() Email format validation
EqualTo('field_name') Must match another field's value
Optional() Allows field to be empty (skips subsequent validators)

Using Forms in View Functions

Example

# File path: app/blueprints/auth.py (refactored register and login)

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(f'Registration successful, welcome {user.username}!','success')

return redirect(url_for('main.index'))

# On GET or failed validation, render template with 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('Invalid username or password.','error')

return render_template('login.html', form=form)

login_user(user, remember=form.remember.data)

 flash(f'Welcome 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

<!-- File path: app/templates/register.html -->

 {% extends 'base.html' %}

{% block title %}Register - TUTORIAL Blog{% endblock %}

{% block content %}

<div class="auth-form">

<h2>Register</h2>

<form method="post">

<!-- form.hidden_tag() automatically generates 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') }}">Log in now</a>

</p>

</div>

 {% endblock %}

Flask-WTF's CSRF protection is automatic: form.hidden_tag() generates a hidden <input> containing a random Token, which the server validates before accepting POST requests. Any form submitted from an external site will be rejected because it lacks the correct Token.


Manual request.form vs Flask-WTF Comparison

Aspect Manual request.form Flask-WTF
Lines of code Repetitive get/validate/report steps for each field Define form class once
CSRF protection Must implement manually Automatic
Custom validation Manual if/else validate_xxx methods
Error display Manual concatenation form.field.errors automatically collected
Applicable scenarios Simple forms (e.g., search box) Complex forms (registration, login, profile editing)

Chapter Summary

In this chapter, you refactored the authentication forms with Flask-WTF: FlaskForm for defining fields and validators, validate_xxx for custom validation, form.validate_on_submit() for unified POST validation, form.hidden_tag() for automatic CSRF protection, and rendering error messages in templates.

Form handling is now more standardized and secure.

← Flask Blog DeployVue3 Blog Lifecycle Fetch β†’