Difficulty: Intermediate
WTForms provides a rich set of field types and validators that cover most form handling needs. Understanding the available fields and validators lets you build complex forms with robust validation quickly.
Field types determine how data is rendered and parsed. StringField creates a text input and returns a string. IntegerField creates a number input and converts to int. FloatField handles decimals. BooleanField creates a checkbox and returns True/False. DateField parses date strings. DateTimeField handles datetime. FileField and MultipleFileField handle file uploads.
Text fields include StringField (single line), TextAreaField (multi-line), and PasswordField (masked input). HiddenField stores hidden values. URLField and EmailField are like StringField but with semantic meaning for HTML5 validation.
Choice fields include SelectField (dropdown), RadioField (radio buttons), and SelectMultipleField (multi-select). Their choices parameter accepts a list of (value, label) tuples. The coerce parameter converts the submitted value to the specified type (default is string).
Validators ensure submitted data meets your requirements. Built-in validators include DataRequired (field must not be empty), Length(min, max) (string length bounds), Email (valid email format), URL (valid URL format), NumberRange(min, max) (number bounds), EqualTo(field) (must match another field), Regexp(pattern) (must match a regex), AnyOf(values) (must be one of), NoneOf(values) (must not be one of), and Optional (field can be empty).
The Optional validator is important for fields that can be left blank. Without it, empty fields still trigger other validators. With Optional(), the field is only validated if it has data. InputRequired is stricter than DataRequired: DataRequired checks the Python value is truthy, while InputRequired checks that raw data was submitted.
Validators can have custom error messages. Pass message='Your custom message' to any validator. This lets you provide user-friendly, context-specific error messages instead of generic defaults.
You can chain multiple validators on a single field. They are checked in order, and all errors are collected. If DataRequired fails, subsequent validators are still checked (unless the field has the StopValidation flag). Use the stop_on_first_error flag on individual validators to short-circuit.
from flask_wtf import FlaskForm
from wtforms import (
StringField, PasswordField, IntegerField, FloatField,
BooleanField, TextAreaField, DateField, SelectField,
HiddenField, URLField, SubmitField
)
from wtforms.validators import DataRequired, Optional
class ProductForm(FlaskForm):
name = StringField('Product Name', validators=[DataRequired()])
description = TextAreaField('Description')
price = FloatField('Price', validators=[DataRequired()])
quantity = IntegerField('Quantity', default=0)
category = SelectField('Category', choices=[
('electronics', 'Electronics'),
('clothing', 'Clothing'),
('books', 'Books')
])
on_sale = BooleanField('On Sale')
sale_price = FloatField('Sale Price', validators=[Optional()])
website = URLField('Product URL', validators=[Optional()])
release_date = DateField('Release Date', format='%Y-%m-%d',
validators=[Optional()])
submit = SubmitField('Save Product')
Different field types handle different data types. FloatField auto-converts to float, IntegerField to int. SelectField renders a dropdown. Optional() allows fields to be left empty without triggering validation.
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, PasswordField
from wtforms.validators import (
DataRequired, Length, Email, URL, NumberRange,
EqualTo, Regexp, AnyOf, NoneOf, Optional, InputRequired
)
class AdvancedForm(FlaskForm):
# Required with custom message
username = StringField('Username', validators=[
DataRequired(message='Username is required'),
Length(min=3, max=20),
Regexp(r'^[a-zA-Z0-9_]+#39;,
message='Only letters, numbers, and underscores')
])
# Number range
age = IntegerField('Age', validators=[
DataRequired(),
NumberRange(min=13, max=120, message='Must be 13-120')
])
# Password with confirmation
password = PasswordField('Password', validators=[
InputRequired(),
Length(min=8)
])
confirm = PasswordField('Confirm', validators=[
InputRequired(),
EqualTo('password', message='Passwords must match')
])
# Must be from a set
role = StringField('Role', validators=[
AnyOf(['admin', 'user', 'moderator'])
])
Validators are composable and each can have custom messages. Regexp validates against regex patterns. NumberRange checks numeric bounds. EqualTo compares two fields. AnyOf restricts to a set of values.
from flask_wtf import FlaskForm
from wtforms import SelectField, StringField
from wtforms.validators import DataRequired
class AssignTaskForm(FlaskForm):
title = StringField('Task Title', validators=[DataRequired()])
assignee = SelectField('Assign To', coerce=int)
# In the view, set choices dynamically from database
@app.route('/tasks/new', methods=['GET', 'POST'])
def new_task():
form = AssignTaskForm()
# Load choices from database
users = User.query.all()
form.assignee.choices = [(u.id, u.name) for u in users]
if form.validate_on_submit():
task = Task(
title=form.title.data,
assignee_id=form.assignee.data # Already an int due to coerce
)
db.session.add(task)
db.session.commit()
return redirect(url_for('tasks'))
return render_template('new_task.html', form=form)
Set choices dynamically in the view function, not in the form class. The coerce=int parameter converts the submitted string value to an integer. This is essential when choice values are database IDs.
<!-- Template rendering with attributes -->
<!-- Basic rendering -->
{{ form.username() }}
<!-- <input id="username" name="username" type="text" value=""> -->
<!-- With CSS classes and attributes -->
{{ form.username(class='form-control', placeholder='Enter username',
autofocus=true, maxlength=20) }}
<!-- <input class="form-control" id="username" maxlength="20"
name="username" placeholder="Enter username"
type="text" autofocus value=""> -->
<!-- With data attributes -->
{{ form.email(class='form-control',
{'data-validate': 'email', 'aria-label': 'Email'}) }}
<!-- Rendering errors -->
{% if form.username.errors %}
<div class="invalid-feedback">
{{ form.username.errors[0] }}
</div>
{% endif %}
Fields accept HTML attributes as keyword arguments. Use double-star unpacking for attributes with hyphens (like data-*). Access validation errors through form.field.errors to display them next to the input.
Field Types, Built-in Validators, Validator Chaining, Custom Messages, Optional Fields