Flask HTML Forms
WTForms is a Python library that defines forms as Python classes instead of raw HTML. Flask-WTF wraps WTForms to integrate it with Flask and adds CSRF protection automatically. This approach validates data on the server and keeps form logic out of your HTML templates.
Why WTForms Over Plain HTML Forms
| Feature | Plain HTML Form | WTForms |
|---|---|---|
| Validation | Manual checks in route | Declared in the form class |
| CSRF protection | Must add manually | Built-in automatically |
| Reuse | Copy-paste HTML | Import the class anywhere |
| Error messages | Write yourself | Generated by validators |
Installing Flask-WTF
pip install flask-wtfConfiguring a Secret Key
CSRF protection requires a secret key. Flask uses it to sign tokens. Set it in your app configuration:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-very-secret-key-here'In production, load the secret key from an environment variable, never hardcode it in your source code.
Defining a Form Class
Create a file called forms.py in your project:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
submit = SubmitField('Log In')Each field maps to an HTML input type. Validators run automatically when the form is submitted.
Common Field Types
| WTForms Field | HTML Equivalent |
|---|---|
StringField | <input type="text"> |
PasswordField | <input type="password"> |
EmailField | <input type="email"> |
TextAreaField | <textarea> |
BooleanField | <input type="checkbox"> |
SelectField | <select> |
IntegerField | <input type="number"> |
SubmitField | <button type="submit"> |
Using the Form in a Route
from flask import Flask, render_template, redirect, url_for
from forms import LoginForm
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
email = form.email.data
password = form.password.data
# authenticate user...
return redirect(url_for('dashboard'))
return render_template('login.html', form=form)form.validate_on_submit() returns True only when the request is POST and all validators pass. On GET requests, it returns False and the form displays. On a failed POST (invalid data), it also returns False and the form redisplays with error messages.
Rendering the Form in a Template
Create templates/login.html:
<h1>Login</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div>
{{ form.email.label }}
{{ form.email(placeholder="you@example.com") }}
{% for error in form.email.errors %}
<span style="color:red">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.password.label }}
{{ form.password() }}
{% for error in form.password.errors %}
<span style="color:red">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit() }}
</form>{{ form.hidden_tag() }} inserts the CSRF token as a hidden input. This token verifies that the form submission came from your own site, not from an attacker's page.
What CSRF Protection Does
Without CSRF: Attacker's page ──POST /transfer?amount=1000──▶ Your bank With CSRF token: Attacker's page ──POST (no valid token)──▶ Flask rejects: 400 Bad Request Your own page ──POST (valid token)────▶ Flask accepts: processes form
Summary
Flask-WTF brings form classes, automatic validation, and CSRF protection to Flask. Define form fields and validators in a Python class. Pass a form instance to render_template(). Call form.validate_on_submit() in the route to handle valid submissions. Render each field and its errors in the template using Jinja2. This approach scales better than manual HTML forms as your application grows.
