Creating Form Classes
A Flask-WTF form class defines:
- fields
- validators
- default values
Example: Contact form
Section titled “Example: Contact form”from flask_wtf import FlaskForm
from wtforms import StringField, EmailField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class ContactForm(FlaskForm):
name = StringField("Name", validators=[DataRequired(), Length(min=2, max=50)])
email = EmailField("Email", validators=[DataRequired(), Email()])
submit = SubmitField("Send")Using the form in a view
Section titled “Using the form in a view”from flask import Flask, render_template, redirect, url_for
app = Flask(__name__)
app.config["SECRET_KEY"] = "dev-key"
@app.route("/contact", methods=["GET", "POST"])
def contact():
form = ContactForm()
if form.validate_on_submit():
# Access clean field values:
name = form.name.data
email = form.email.data
# ...save/send email...
return redirect(url_for("contact_success"))
return render_template("contact.html", form=form)Why validate_on_submit()?
Section titled “Why validate_on_submit()?”It’s shorthand for:
- request is POST
- AND form validates
This keeps your route clean.
A form is a class, and the fields are its schema
Section titled “A form is a class, and the fields are its schema”from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, SelectField, SubmitField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
class SignupForm(FlaskForm):
name = StringField("Name", validators=[DataRequired(), Length(min=2, max=20)])
age = IntegerField("Age", validators=[Optional(), NumberRange(min=0, max=130)])
role = SelectField("Role", choices=[("u", "User"), ("a", "Admin")])
go = SubmitField("Sign up")Each attribute declares three things at once — the HTML control, the Python type the value is coerced to, and the rules it must satisfy:
flowchart LR
D["StringField('Name',
validators=[DataRequired(), Length(2, 20)])"] --> H["renders an input"]
D --> T["coerces the raw string"]
D --> V["validates on submit"]
V --> E["field.errors"]
Validators run in order and accumulate
Section titled “Validators run in order and accumulate”Measured error dictionaries for a form with name, age and email:
| submitted | form.errors |
|---|---|
name='a', bad email | name: ['Field must be between 2 and 20 characters long.'], email: ['Invalid email address.'] |
age='999' | age: ['Number must be between 0 and 130.'] |
age='abc' | age: ['Not a valid integer value.', 'Number must be between 0 and 130.'] |
name missing | name: ['This field is required.'] |
Two details worth reading off that table:
- Errors are collected per field, and a field can hold more than one.
age='abc'produced two, because coercion failed and the range check then ran against the resultingNone. - Validation does not stop at the first failing field.
form.errorsis the complete picture, which is what lets you re-render every problem at once.
DataRequired is not InputRequired
Section titled “DataRequired is not InputRequired”DataRequired() # fails if the coerced value is FALSY: '', 0, False, None
InputRequired() # fails only if nothing was submitted for the fieldFor a number that may legitimately be 0, or a checkbox that may legitimately be
unchecked, DataRequired rejects a valid answer. Use InputRequired when the question
is “did they send this field”, and DataRequired when it is “is there a meaningful
value”.
Custom validation
Section titled “Custom validation”from wtforms.validators import ValidationError
class SignupForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
def validate_name(self, field): # validate_<fieldname>
if User.query.filter_by(name=field.data).first():
raise ValidationError("That name is taken.")A method called validate_<fieldname> is picked up automatically and runs after that
field’s validator list. Raising ValidationError appends to field.errors exactly like
a built-in.
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
Submitting age=abc to an IntegerField with NumberRange(0, 130) produced two errors on that field. Why two?
Measured: ['Not a valid integer value.', 'Number must be between 0 and 130.']. Errors accumulate per field rather than stopping at the first.
pch.quizShowAnswer
B — coercion to int failed, and the range validator then ran against the resulting None — Measured: ['Not a valid integer value.', 'Number must be between 0 and 130.']. Errors accumulate per field rather than stopping at the first.
-
Why should Optional() be first in a validators list?
List order is execution order. Placed after NumberRange, the range check runs first on an empty value and reports a spurious error.
pch.quizShowAnswer
B — it stops the chain when the field is empty, so later validators do not fire on an empty value — List order is execution order. Placed after NumberRange, the range check runs first on an empty value and reports a spurious error.
-
Which validator should guard a number field where 0 is a legitimate answer?
DataRequired fails on any falsy coerced value, so it rejects 0, an empty string and an unchecked box. InputRequired only asks whether the field was submitted.
pch.quizShowAnswer
B — InputRequired, because DataRequired rejects falsy values including 0 — DataRequired fails on any falsy coerced value, so it rejects 0, an empty string and an unchecked box. InputRequired only asks whether the field was submitted.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading