Flask WTForms

Form validation checks that user input meets your requirements before your application processes or stores it. WTForms provides a set of built-in validators. You can also write custom validators for business-specific rules.

Why Validate on the Server

Browser-side validation (HTML required, type="email") is a courtesy to the user — it gives instant feedback. Server-side validation is the security gate — it catches malicious or malformed data that bypasses the browser. Always validate on the server even if the browser validates too.

Browser validation: Nice to have (user experience)
Server validation:  Mandatory (security and data integrity)

Built-In WTForms Validators

ValidatorWhat It ChecksExample
DataRequired()Field is not emptyName field must have a value
Email()Valid email formatuser@example.com
Length(min=2, max=50)String length within rangeUsername 2–50 characters
EqualTo('field')Matches another fieldPassword confirmation
NumberRange(min=1, max=100)Number within rangeAge between 1 and 100
URL()Valid URL formathttps://example.com
Regexp(r'^\d{5}$')Matches a regex pattern5-digit ZIP code
Optional()Field is optionalSkips other validators if empty

Registration Form with Validators

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

class RegisterForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(message='Username is required.'),
        Length(min=3, max=20, message='Username must be 3–20 characters.')
    ])

    email = EmailField('Email', validators=[
        DataRequired(),
        Email(message='Enter a valid email address.')
    ])

    password = PasswordField('Password', validators=[
        DataRequired(),
        Length(min=8, message='Password must be at least 8 characters.')
    ])

    confirm = PasswordField('Confirm Password', validators=[
        DataRequired(),
        EqualTo('password', message='Passwords must match.')
    ])

    submit = SubmitField('Create Account')

How Validators Run

User submits form
       │
form.validate_on_submit() called
       │
WTForms runs each validator in order for each field
       │
  All pass?  ──YES──▶  returns True  ──▶  process form
       │
      NO
       │
field.errors list filled with error messages
       │
returns False  ──▶  redisplay form with errors

Displaying Errors per Field

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

  {{ form.username.label }}
  {{ form.username() }}
  {% for err in form.username.errors %}
    <p style="color:red">{{ err }}</p>
  {% endfor %}

  {{ form.email.label }}
  {{ form.email() }}
  {% for err in form.email.errors %}
    <p style="color:red">{{ err }}</p>
  {% endfor %}

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

  {{ form.confirm.label }}
  {{ form.confirm() }}
  {% for err in form.confirm.errors %}
    <p style="color:red">{{ err }}</p>
  {% endfor %}

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

Writing a Custom Validator

Custom validators are functions that raise ValidationError when the input fails the check. Attach them to a field alongside the built-in validators:

from wtforms import ValidationError

def no_spaces(form, field):
    if ' ' in field.data:
        raise ValidationError('Username cannot contain spaces.')

class RegisterForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        Length(min=3, max=20),
        no_spaces
    ])

Method Validator on the Form Class

WTForms also recognizes methods named validate_fieldname on the form class. These run automatically after the field's listed validators:

class RegisterForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])

    def validate_username(self, field):
        # Check if username already exists in database
        existing = User.query.filter_by(username=field.data).first()
        if existing:
            raise ValidationError('Username already taken. Choose another.')

Accessing Cleaned Data

After successful validation, field.data holds the cleaned, validated value. For string fields, WTForms strips leading and trailing whitespace automatically.

if form.validate_on_submit():
    username = form.username.data   # cleaned string
    email    = form.email.data
    password = form.password.data
    # now safe to save to database

Summary

Validation protects your database from bad data and your users from confusing errors. WTForms validators run automatically when you call validate_on_submit(). Built-in validators cover the most common cases. Custom validators and validate_fieldname methods handle business-specific rules. After validation passes, access clean data through form.fieldname.data and store it with confidence.

Leave a Comment

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