Skip to main content

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.

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

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

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

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

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

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

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

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

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

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

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

python
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

  1. Keep validators focused: Each validator should check one specific condition
  2. Provide clear error messages: Users should understand exactly what's wrong
  3. Handle exceptions gracefully: Prevent unexpected crashes during validation
  4. Reuse validators: Create reusable validators for common validation scenarios
  5. 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

  1. Create a custom validator that checks if a date field is a weekday (not Saturday or Sunday)
  2. Implement a validator that ensures a username contains only letters, numbers, and underscores
  3. Build a validator that checks if a ZIP code matches a specific country's format
  4. 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! :)