Flask WTForms Basics
When building web applications with Flask, you'll frequently need to handle user input through forms. While you could create HTML forms manually and process the inputs yourself, Flask-WTF and WTForms provide a more elegant, secure, and efficient approach.
Introduction to Flask-WTF
Flask-WTF is an extension that integrates WTForms with Flask. WTForms is a forms validation and rendering library for Python web applications. Combined, they offer:
- Form validation
- CSRF protection
- File uploads
- Internationalization
- Integration with various Flask extensions
Let's learn how to use Flask-WTF to create and manage forms in your Flask applications.
Setting Up Flask-WTF
First, you need to install the Flask-WTF package:
pip install Flask-WTF
To use it in your Flask application, you'll need to configure a secret key for CSRF protection:
from flask import Flask, render_template
from flask_wtf import FlaskForm
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key' # Replace with a strong random key in production
Creating Your First Form
Let's create a simple login form:
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=8)])
submit = SubmitField('Log In')
Breaking down the form:
- We inherit from
FlaskForm
(provided by Flask-WTF) - Each form field is an attribute of the form class
- Fields have a label (first argument) and optional validators
- Validators ensure the data meets specific criteria
Rendering the Form in a Template
You can pass your form to a template and render it:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
return render_template('login.html', form=form)
In your template (login.html):
{% extends 'base.html' %}
{% block content %}
<h1>Login</h1>
<form method="POST" action="{{ url_for('login') }}">
{{ form.csrf_token }}
<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="text-danger">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
<div class="text-danger">
{% for error in form.password.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
{% endblock %}
Key points about the template:
- The
form.csrf_token
field adds CSRF protection - We can apply CSS classes to form fields:
form.email(class="form-control")
- Error handling is included with
form.field.errors
Form Validation
A core benefit of WTForms is handling validation. Let's enhance our login route:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# Form data has been submitted and was valid
email = form.email.data
password = form.password.data
# Process login (check credentials, etc.)
return f"Login attempt for {email}"
return render_template('login.html', form=form)
The validate_on_submit()
method:
- Returns
True
if the form was submitted via POST and all validators passed - Returns
False
otherwise - Automatically adds error messages to form fields that failed validation
Common Field Types
WTForms provides many field types for different data:
from wtforms import (StringField, PasswordField, BooleanField,
TextAreaField, SelectField, IntegerField,
DateField, FileField)
class ExampleForm(FlaskForm):
username = StringField('Username')
password = PasswordField('Password')
remember_me = BooleanField('Remember Me')
bio = TextAreaField('Biography')
country = SelectField('Country', choices=[
('us', 'United States'),
('uk', 'United Kingdom'),
('ca', 'Canada')
])
age = IntegerField('Age')
birthday = DateField('Birthday', format='%Y-%m-%d')
profile_pic = FileField('Profile Picture')
Common Validators
Validators ensure the data meets certain criteria:
from wtforms.validators import (
DataRequired, Email, Length, EqualTo,
NumberRange, URL, Regexp
)
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(message="Username is required"),
Length(min=3, max=20, message="Username must be 3-20 characters long")
])
email = StringField('Email', validators=[
DataRequired(),
Email(message="Please enter a valid email address")
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8, message="Password must be at least 8 characters")
])
confirm_password = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message="Passwords must match")
])
age = IntegerField('Age', validators=[
NumberRange(min=18, message="You must be at least 18 years old")
])
website = StringField('Website', validators=[
URL(message="Please enter a valid URL")
])
phone = StringField('Phone', validators=[
Regexp(r'^\d{3}-\d{3}-\d{4}$', message="Phone must be in format XXX-XXX-XXXX")
])
Custom Validators
Sometimes you'll need custom validation logic:
from wtforms.validators import ValidationError
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
# Custom validator as a method
def validate_username(self, username):
forbidden_usernames = ['admin', 'root', 'system']
if username.data.lower() in forbidden_usernames:
raise ValidationError(f"Username '{username.data}' is reserved.")
Practical Example: A User Registration Form
Let's create a more complete user registration example:
from flask import Flask, render_template, redirect, url_for, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, EmailField
from wtforms.validators import DataRequired, Email, Length, EqualTo
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev-key-for-learning'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=20)
])
email = EmailField('Email', validators=[
DataRequired(),
Email()
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8)
])
confirm_password = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password')
])
submit = SubmitField('Register')
# Custom validation to check if username exists
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username already exists. Please choose a different one.')
# Custom validation to check if email exists
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email already registered. Please use a different one.')
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# In a real app, you would hash the password before storing it
user = User(
username=form.username.data,
email=form.email.data,
password=form.password.data
)
db.session.add(user)
db.session.commit()
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
# Create database tables before first request
@app.before_first_request
def create_tables():
db.create_all()
File Uploads with Flask-WTF
Flask-WTF also handles file uploads:
from flask_wtf.file import FileField, FileRequired, FileAllowed
class PhotoForm(FlaskForm):
photo = FileField('Upload Photo', validators=[
FileRequired(),
FileAllowed(['jpg', 'png', 'jpeg'], 'Images only!')
])
submit = SubmitField('Upload')
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = PhotoForm()
if form.validate_on_submit():
filename = secure_filename(form.photo.data.filename)
form.photo.data.save('uploads/' + filename)
flash('Photo uploaded successfully!')
return redirect(url_for('upload'))
return render_template('upload.html', form=form)
Form Inheritance
You can create base forms and inherit from them:
class BaseUserForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
class RegistrationForm(BaseUserForm):
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password', validators=[EqualTo('password')])
submit = SubmitField('Register')
class ProfileUpdateForm(BaseUserForm):
bio = TextAreaField('Biography')
submit = SubmitField('Update Profile')
Summary
Flask-WTF and WTForms provide a powerful framework for handling forms in your Flask applications:
- Form classes define structure and validation rules
- Validators ensure data integrity
- CSRF protection is built-in
- Template integration makes rendering forms easy
- Custom validators allow complex validation logic
These tools help you create secure, user-friendly forms with less code and fewer security vulnerabilities than handling forms manually.
Additional Resources
- Official WTForms Documentation
- Flask-WTF Documentation
- Miguel Grinberg's Flask Mega-Tutorial (Forms Chapter)
Practice Exercises
- Create a contact form with name, email, subject, and message fields
- Build a form for a blog post with title, content, category (select field), and tags
- Implement a password change form that requires the current password and validation of the new password
- Create a form with dynamic choices (where the select options come from a database)
- Build a multi-page form wizard using Flask sessions to store intermediate data
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)