Forms and Flask-WTF
Introduction
HTML forms send user input to your server via POST. Reading request.form works for simple cases, but production apps need validation, error messages, and CSRF protection. Flask-WTF integrates WTForms with Flask sessions so you build secure forms with less boilerplate.
Prerequisites
- Jinja2 Templates
- Configuration and SECRET_KEY—CSRF depends on
SECRET_KEY pip install Flask-WTF
Manual Form Handling
Code explanation:
- You validate every field yourself and re-render the template on failure
- Works for learning—gets repetitive as forms grow
templates/contact.html (simplified):
<form method="post">
<input name="name" value="{{ name }}">
<input name="email" type="email">
<button type="submit">Send</button>
{% for error in errors %}
<p class="error">{{ error }}</p>
{% endfor %}
</form>Install Flask-WTF
pip install Flask-WTFAdd to requirements.txt:
Flask-WTF==1.2.1
WTForms==3.1.2Define a Form Class
forms.py:
Code explanation:
FlaskFormsubclassesFormand adds CSRF integrationvalidatorsrun on the server when the form is submitted
View With validate_on_submit
Code explanation:
validate_on_submit()isTrueonly on POST when all validators passform.name.datareads the cleaned field value
Template With CSRF Token
templates/contact_wtf.html:
Code explanation:
form.hidden_tag()renders the CSRF token field—required on every POST form- Field-level
form.name.errorslists validation messages
Warning
CSRF Token Missing
If you omit hidden_tag(), Flask-WTF rejects POST with 400 Bad Request or CSRF error.
CSRF Configuration
app.config["WTF_CSRF_ENABLED"] = True
app.config["WTF_CSRF_TIME_LIMIT"] = NoneDisable only in tests with WTF_CSRF_ENABLED = False in TestingConfig—never in production.
Custom Validators
from wtforms import ValidationError
def no_spam(form, field):
if "http://" in field.data.lower():
raise ValidationError("Links are not allowed in messages.")
class ContactForm(FlaskForm):
message = TextAreaField(
"Message",
validators=[DataRequired(), no_spam],
)Select and Boolean Fields
from wtforms import SelectField, BooleanField
class SettingsForm(FlaskForm):
theme = SelectField(
"Theme",
choices=[("light", "Light"), ("dark", "Dark")],
)
newsletter = BooleanField("Subscribe to newsletter")
submit = SubmitField("Save")FAQ
CSRF token missing error?
Ensure SECRET_KEY is set and template includes {{ form.hidden_tag() }}.
Form validates on GET?
Use validate_on_submit(), not form.validate() alone on GET.
Repopulate fields after error?
Flask-WTF keeps submitted data in form.field.data automatically when validation fails.
AJAX JSON forms?
Flask-WTF targets HTML forms—APIs use JSON validation libraries instead.
Multiple forms on one page?
Pass form=MainForm() and use separate endpoints or prefix on fields.
File upload with WTF?
Use FileField—see file uploads chapter.