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:
pip install Flask-WTF
Once installed, you'll need to configure your Flask application with a secret key:
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:
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 emptyLength()
: Checks that the input has a minimum and/or maximum lengthEmail()
: Validates that the input follows email formatting rulesEqualTo()
: Ensures the field matches another field (useful for password confirmation)
Implementing Validation in Routes
Now let's implement a route to handle this form:
@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:
- Checks if the request method is POST
- Runs all the validators on each field
- 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):
{% 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
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
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:
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:
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:
# 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')
# 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)
<!-- 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:
<!-- 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:
Validator | Purpose |
---|---|
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
- Create a login form with validation for username/email and password
- Build a user profile form that validates different data types (phone numbers, addresses, etc.)
- Implement a form with dynamic validation rules that change based on user selections
- Create a multi-page form wizard with validation at each step
- 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! :)