Flask Custom Validators
When building web applications with Flask, form validation is crucial for ensuring that user input meets your application's requirements. While Flask-WTF provides many built-in validators, there are often cases where you need to create custom validation rules. This guide will walk you through creating and implementing custom validators in Flask forms.
Understanding Custom Validators
Custom validators allow you to define specific validation rules tailored to your application's needs. They're particularly useful when:
- Built-in validators don't address your specific requirements
- You need to perform complex validation logic
- You want to validate against database records or external APIs
- You need to compare multiple fields in a form
Prerequisites
Before diving into custom validators, you should be familiar with:
- Basic Flask application structure
- Creating forms with Flask-WTF
- Using built-in WTForms validators
Basic Custom Validators
Method 1: Custom Validator Functions
The simplest way to create a custom validator is to define a function that raises a ValidationError
when validation fails.
from wtforms.validators import ValidationError
def validate_even_number(form, field):
"""Custom validator to ensure the field value is an even number."""
try:
value = int(field.data)
if value % 2 != 0:
raise ValidationError('Field must be an even number.')
except ValueError:
raise ValidationError('Field must be a valid integer.')
To use this validator in a form:
from flask_wtf import FlaskForm
from wtforms import IntegerField, SubmitField
from wtforms.validators import DataRequired
class EvenNumberForm(FlaskForm):
even_number = IntegerField('Enter an even number',
validators=[DataRequired(), validate_even_number])
submit = SubmitField('Submit')
Method 2: Class-Based Validators
For more complex validators or those that need parameters, class-based validators are preferred:
from wtforms.validators import ValidationError
class DivisibleBy:
"""Custom validator to ensure the field value is divisible by a specified number."""
def __init__(self, divisor, message=None):
self.divisor = divisor
self.message = message or f'Number must be divisible by {divisor}.'
def __call__(self, form, field):
try:
value = int(field.data)
if value % self.divisor != 0:
raise ValidationError(self.message)
except ValueError:
raise ValidationError('Field must be a valid integer.')
To use this class-based validator:
class DivisibleNumberForm(FlaskForm):
number = IntegerField('Enter a number',
validators=[DataRequired(), DivisibleBy(3, 'Must be divisible by 3.')])
submit = SubmitField('Submit')
Practical Examples
Example 1: Username Uniqueness Validator
When registering new users, you typically need to ensure usernames are unique:
from wtforms.validators import ValidationError
from your_app.models import User # Import your User model
class UniqueUsername:
"""Validates that a username is not already taken."""
def __init__(self, message=None):
self.message = message or 'This username is already taken.'
def __call__(self, form, field):
# Check if a user with this username already exists
user = User.query.filter_by(username=field.data).first()
if user:
raise ValidationError(self.message)
Registration form implementation:
class RegistrationForm(FlaskForm):
username = StringField('Username',
validators=[DataRequired(),
Length(min=3, max=25),
UniqueUsername()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=8)])
confirm_password = PasswordField('Confirm Password',
validators=[DataRequired(),
EqualTo('password')])
submit = SubmitField('Register')
Example 2: Password Strength Validator
To ensure users create strong passwords:
import re
from wtforms.validators import ValidationError
class StrongPassword:
"""Validates that a password meets strength requirements."""
def __init__(self, message=None):
self.message = message or 'Password must contain at least one uppercase letter, one lowercase letter, one digit, and one special character.'
def __call__(self, form, field):
password = field.data
# Check for minimum requirements
if not re.search(r'[A-Z]', password):
raise ValidationError('Password must contain at least one uppercase letter.')
if not re.search(r'[a-z]', password):
raise ValidationError('Password must contain at least one lowercase letter.')
if not re.search(r'[0-9]', password):
raise ValidationError('Password must contain at least one digit.')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
raise ValidationError('Password must contain at least one special character.')
Example 3: Comparing Fields
Sometimes you need to validate one field based on another field's value:
from wtforms.validators import ValidationError
class GreaterThan:
"""Validates that field's value is greater than another field's value."""
def __init__(self, field_name, message=None):
self.field_name = field_name
self.message = message or f'Field must be greater than {field_name}.'
def __call__(self, form, field):
try:
compare_with = form[self.field_name].data
if field.data <= compare_with:
raise ValidationError(self.message)
except KeyError:
raise ValidationError(f'Field {self.field_name} not found in form')
except ValueError:
raise ValidationError('Invalid comparison')
Using it in a price range form:
class PriceRangeForm(FlaskForm):
min_price = FloatField('Minimum Price', validators=[DataRequired(), NumberRange(min=0)])
max_price = FloatField('Maximum Price',
validators=[DataRequired(),
NumberRange(min=0),
GreaterThan('min_price', 'Maximum price must be greater than minimum price.')])
submit = SubmitField('Search')
Implementing in Flask Routes
Here's how to implement these forms in your Flask routes:
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Create new user with validated form data
user = User(username=form.username.data,
email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('Account created successfully!', 'success')
return redirect(url_for('login'))
return render_template('register.html', title='Register', form=form)
Combining Multiple Validators
You can combine custom validators with built-in validators:
class ProductForm(FlaskForm):
name = StringField('Product Name',
validators=[DataRequired(),
Length(min=3, max=50)])
sku = StringField('SKU',
validators=[DataRequired(),
Length(min=5, max=20),
UniqueProductSKU()])
price = FloatField('Price',
validators=[DataRequired(),
NumberRange(min=0.01,
message='Price must be greater than 0.')])
submit = SubmitField('Add Product')
Advanced: Form-Level Validators
Sometimes you need to validate across multiple fields. You can do this by implementing validate()
method in your form:
class TimeRangeForm(FlaskForm):
start_time = TimeField('Start Time', validators=[DataRequired()])
end_time = TimeField('End Time', validators=[DataRequired()])
submit = SubmitField('Submit')
def validate(self):
# First run the standard validators
if not super(TimeRangeForm, self).validate():
return False
# Then do our custom validation
if self.start_time.data >= self.end_time.data:
self.end_time.errors.append('End time must be after start time.')
return False
return True
Best Practices for Custom Validators
- Keep validators focused: Each validator should check one specific condition
- Provide clear error messages: Users should understand exactly what's wrong
- Handle exceptions gracefully: Prevent unexpected crashes during validation
- Reuse validators: Create reusable validators for common validation scenarios
- Test thoroughly: Ensure your validators work with both valid and invalid data
Summary
Custom validators in Flask enhance your form validation capabilities by allowing you to define specific rules tailored to your application's needs. Whether you're validating against database records, implementing complex business rules, or ensuring data integrity, custom validators provide a clean, reusable approach to form validation.
By using either function-based validators for simple cases or class-based validators for more complex scenarios, you can ensure that your Flask application collects high-quality data while providing clear feedback to users when their input doesn't meet requirements.
Exercises
- Create a custom validator that checks if a date field is a weekday (not Saturday or Sunday)
- Implement a validator that ensures a username contains only letters, numbers, and underscores
- Build a validator that checks if a ZIP code matches a specific country's format
- Create a form-level validator for a reservation form that ensures a room isn't booked twice for the same date range
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)