Skip to main content

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:

bash
pip install Flask-WTF

To use it in your Flask application, you'll need to configure a secret key for CSRF protection:

python
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:

python
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:

  1. We inherit from FlaskForm (provided by Flask-WTF)
  2. Each form field is an attribute of the form class
  3. Fields have a label (first argument) and optional validators
  4. Validators ensure the data meets specific criteria

Rendering the Form in a Template

You can pass your form to a template and render it:

python
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
return render_template('login.html', form=form)

In your template (login.html):

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:

python
@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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
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

Practice Exercises

  1. Create a contact form with name, email, subject, and message fields
  2. Build a form for a blog post with title, content, category (select field), and tags
  3. Implement a password change form that requires the current password and validation of the new password
  4. Create a form with dynamic choices (where the select options come from a database)
  5. 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! :)