Skip to main content

Flask Form Validation

When building web applications that accept user input, validating that input is crucial for security and functionality. In this tutorial, you'll learn how to implement proper form validation in Flask applications using WTForms.

Introduction to Form Validation

Form validation is the process of checking whether the data submitted by users meets specific criteria before processing it. Validation helps:

  • Prevent malicious inputs that could lead to security vulnerabilities
  • Ensure data integrity in your database
  • Provide a better user experience by immediately notifying users of input errors
  • Reduce server-side errors caused by unexpected data formats

Flask doesn't include built-in form validation, but the WTForms library (typically used with Flask-WTF) provides robust validation capabilities that integrate seamlessly with Flask.

Setting Up Flask-WTF

Before we dive into validation, make sure you have Flask-WTF installed:

bash
pip install Flask-WTF

Once installed, you'll need to configure your Flask application with a secret key:

python
from flask import Flask, render_template, redirect, url_for, flash
from flask_wtf import FlaskForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here' # Replace with a real secret key in production

Basic Form Validation

Let's start with a simple registration form example:

python
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo

class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=4, max=20)
])
email = StringField('Email', validators=[
DataRequired(),
Email()
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8)
])
confirm_password = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
])
submit = SubmitField('Sign Up')

In this example, we're using several built-in validators:

  • DataRequired(): Ensures the field is not empty
  • Length(): Checks that the input has a minimum and/or maximum length
  • Email(): Validates that the input follows email formatting rules
  • EqualTo(): Ensures the field matches another field (useful for password confirmation)

Implementing Validation in Routes

Now let's implement a route to handle this form:

python
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Process valid form data
username = form.username.data
email = form.email.data
password = form.password.data
# Here you would typically save to a database
flash(f'Account created for {username}!', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)

The key part here is form.validate_on_submit() which:

  1. Checks if the request method is POST
  2. Runs all the validators on each field
  3. Returns True only if all validations pass

Template with Error Messages

Here's how you would display the form in an HTML template (using Jinja2):

html
{% extends "layout.html" %}

{% block content %}
<div class="form-container">
<h1>Register</h1>
<form method="POST" action="">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label }}
{{ form.username(class="form-control") }}
{% if form.username.errors %}
<div class="error-message">
{% for error in form.username.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="error-message">
{% for error in form.email.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.password.label }}
{{ form.password(class="form-control") }}
{% if form.password.errors %}
<div class="error-message">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.confirm_password.label }}
{{ form.confirm_password(class="form-control") }}
{% if form.confirm_password.errors %}
<div class="error-message">
{% for error in form.confirm_password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
{% endblock %}

Custom Validators

While WTForms provides many useful validators, sometimes you need custom validation logic. Here's how to create custom validators:

Method 1: Custom Validator Functions

python
from wtforms.validators import ValidationError

def validate_username(form, field):
forbidden_usernames = ['admin', 'root', 'system']
if field.data.lower() in forbidden_usernames:
raise ValidationError('Username is reserved and unavailable.')

class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=4, max=20),
validate_username
])
# Other fields...

Method 2: Custom Validation Methods in Form Class

python
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=4, max=20)
])
# Other fields...

def validate_username(self, username):
# Simulate checking against existing users in a database
existing_users = ['john', 'jane', 'alice', 'bob']
if username.data.lower() in existing_users:
raise ValidationError('This username is already taken. Please choose a different one.')

Advanced Validation Techniques

Conditional Validation

Sometimes you need validation that depends on other fields:

python
class PaymentForm(FlaskForm):
payment_method = SelectField('Payment Method',
choices=[('credit', 'Credit Card'), ('paypal', 'PayPal')])
credit_card_number = StringField('Credit Card Number')
paypal_email = StringField('PayPal Email')

def validate(self):
if not super(PaymentForm, self).validate():
return False

if self.payment_method.data == 'credit' and not self.credit_card_number.data:
self.credit_card_number.errors.append('Credit card number required when using credit card payment')
return False

if self.payment_method.data == 'paypal' and not self.paypal_email.data:
self.paypal_email.errors.append('PayPal email required when using PayPal')
return False

return True

Cross-Field Validation

For validations involving multiple fields:

python
class ChangePasswordForm(FlaskForm):
current_password = PasswordField('Current Password', validators=[DataRequired()])
new_password = PasswordField('New Password', validators=[
DataRequired(),
Length(min=8)
])
confirm_password = PasswordField('Confirm New Password', validators=[
DataRequired(),
EqualTo('new_password', message='Passwords must match')
])

def validate_new_password(self, new_password):
if new_password.data == self.current_password.data:
raise ValidationError('New password cannot be the same as your current password.')

Real-World Example: Contact Form with Validation

Let's implement a complete contact form with validation:

python
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, SelectField
from wtforms.validators import DataRequired, Email, Length

class ContactForm(FlaskForm):
name = StringField('Name', validators=[
DataRequired(message="Please enter your name"),
Length(min=2, max=50, message="Name must be between 2 and 50 characters")
])
email = StringField('Email', validators=[
DataRequired(message="Please enter your email"),
Email(message="Please enter a valid email address")
])
subject = SelectField('Subject', choices=[
('', 'Select a subject'),
('general', 'General Inquiry'),
('support', 'Technical Support'),
('feedback', 'Feedback'),
('other', 'Other')
], validators=[
DataRequired(message="Please select a subject")
])
message = TextAreaField('Message', validators=[
DataRequired(message="Please enter your message"),
Length(min=10, max=1000, message="Message must be between 10 and 1000 characters")
])
submit = SubmitField('Send Message')
python
# app.py
from flask import Flask, render_template, redirect, url_for, flash
from forms import ContactForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'

@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
name = form.name.data
email = form.email.data
subject = form.subject.data
message = form.message.data

# In a real application, you might send an email or save to database
flash('Your message has been sent! We will contact you soon.', 'success')
return redirect(url_for('contact'))

return render_template('contact.html', form=form)

if __name__ == '__main__':
app.run(debug=True)
html
<!-- contact.html -->
{% extends "layout.html" %}

{% block content %}
<div class="contact-container">
<h1>Contact Us</h1>

{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}

<form method="POST">
{{ form.hidden_tag() }}

<div class="form-group">
{{ form.name.label }}
{{ form.name(class="form-control") }}
{% if form.name.errors %}
<div class="error-message">
{% for error in form.name.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="error-message">
{% for error in form.email.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.subject.label }}
{{ form.subject(class="form-control") }}
{% if form.subject.errors %}
<div class="error-message">
{% for error in form.subject.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.message.label }}
{{ form.message(class="form-control", rows=5) }}
{% if form.message.errors %}
<div class="error-message">
{% for error in form.message.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<div class="form-group">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
{% endblock %}

Client-Side Validation

While Flask-WTF provides server-side validation, adding client-side validation improves user experience by providing immediate feedback. You can use HTML5 attributes or JavaScript:

html
<!-- Adding HTML5 validation -->
<form method="POST">
{{ form.hidden_tag() }}

<div class="form-group">
{{ form.email.label }}
{{ form.email(class="form-control", type="email", required=true) }}
{% if form.email.errors %}
<div class="error-message">
{% for error in form.email.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>

<!-- Other fields... -->
</form>

Remember that client-side validation is only for user convenience - you should always maintain server-side validation as a security measure.

Common Validators in WTForms

Here's a reference of commonly used validators:

ValidatorPurpose
DataRequired()Field cannot be empty
Email()Must be a valid email format
Length(min, max)Field must be between min and max characters
NumberRange(min, max)For number fields, must be within range
URL()Must be a valid URL
EqualTo(fieldname)Must match the value of another field
Regexp(regex)Must match the regular expression pattern
Optional()Makes preceding validators optional if field is empty
InputRequired()Field is required, but accepts empty strings
AnyOf(values)Field must be one of the specified values
NoneOf(values)Field cannot be any of the specified values

Summary

Form validation is a critical aspect of web development that ensures data integrity and application security. With Flask-WTF, you can:

  • Create forms with validation rules using WTForms validators
  • Implement custom validation logic for specific requirements
  • Display meaningful error messages to users
  • Combine server-side and client-side validation for the best user experience

By properly implementing form validation in your Flask applications, you'll enhance both security and usability, providing a more robust experience for your users.

Additional Resources

Exercises

  1. Create a login form with validation for username/email and password
  2. Build a user profile form that validates different data types (phone numbers, addresses, etc.)
  3. Implement a form with dynamic validation rules that change based on user selections
  4. Create a multi-page form wizard with validation at each step
  5. Add AJAX validation to provide real-time feedback without page reloads

Happy coding!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)