Custom Validators
This section demonstrates how to create custom validators to extend Valid8r’s capabilities for specific validation scenarios. While Valid8r provides many built-in validators, you can easily create your own validators for domain-specific validation needs.
Creating a Simple Custom Validator
Custom validators are functions that take a value and return a Maybe object. To create a custom validator:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
# Create a custom validator for even numbers
def validate_even(value):
if value % 2 == 0:
return Maybe.success(value)
return Maybe.failure("Value must be even")
# Convert to a Validator instance for operator overloading
is_even = validators.Validator(validate_even)
# Use the custom validator with pattern matching
result = is_even(42) # Valid
match result:
case Success(value):
print(f"Valid even number: {value}") # Valid even number: 42
case Failure(_):
print("This won't happen")
# Invalid case
result = is_even(43) # Invalid
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(f"Error: {error}") # Error: Value must be even
Creating a Validator Factory Function
For reusable validators with parameters, create factory functions that return validator instances:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
# Validator factory for divisibility check
def divisible_by(n, error_message=None):
def validator(value):
if value % n == 0:
return Maybe.success(value)
return Maybe.failure(
error_message or f"Value must be divisible by {n}"
)
return validators.Validator(validator)
# Create validators using the factory
is_divisible_by_3 = divisible_by(3)
is_divisible_by_5 = divisible_by(5)
# Process results with pattern matching
def check_divisibility(value):
div3_result = is_divisible_by_3(value)
div5_result = is_divisible_by_5(value)
match (div3_result, div5_result):
case (Success(_), Success(_)):
return f"{value} is divisible by both 3 and 5"
case (Success(_), Failure(_)):
return f"{value} is divisible by 3 but not by 5"
case (Failure(_), Success(_)):
return f"{value} is divisible by 5 but not by 3"
case (Failure(_), Failure(_)):
return f"{value} is divisible by neither 3 nor 5"
# Test with different values
for value in [9, 10, 15, 7]:
print(check_divisibility(value))
# Custom error message
is_multiple_of_10 = divisible_by(10, "Must be a multiple of 10")
result = is_multiple_of_10(15)
match result:
case Success(_):
print("This won't happen")
case Failure(error):
print(f"Error: {error}") # Error: Must be a multiple of 10
String Validation Examples
Custom validators for common string validation scenarios:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
import re
# Email validation using matches_regex
def email_validator(error_message=None):
return validators.matches_regex(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
error_message=error_message or "Invalid email format"
)
# URL validation using matches_regex
def url_validator(error_message=None):
return validators.matches_regex(
r"^https?://(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$",
error_message=error_message or "Invalid URL format"
)
# Phone number validation using matches_regex
def phone_validator(country="international", error_message=None):
patterns = {
"us": r"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$",
"international": r"^\+[1-9]\d{1,14}$"
}
pattern = patterns.get(country.lower(), patterns["international"])
return validators.matches_regex(
pattern,
error_message=error_message or f"Invalid phone number for {country} format"
)
# Validate contact information with pattern matching
def validate_contact_info(email, url, phone):
is_valid_email = email_validator()
is_valid_url = url_validator()
is_valid_us_phone = phone_validator("us")
email_result = is_valid_email(email)
url_result = is_valid_url(url)
phone_result = is_valid_us_phone(phone)
# Process all validation results at once
match (email_result, url_result, phone_result):
case (Success(e), Success(u), Success(p)):
return {
"status": "valid",
"contact": {
"email": e,
"website": u,
"phone": p
}
}
case (Failure(error), _, _):
return {
"status": "invalid",
"field": "email",
"error": error
}
case (_, Failure(error), _):
return {
"status": "invalid",
"field": "website",
"error": error
}
case (_, _, Failure(error)):
return {
"status": "invalid",
"field": "phone",
"error": error
}
# Test with valid data
result = validate_contact_info(
"user@example.com",
"https://example.com",
"555-123-4567"
)
print(result["status"]) # valid
# Test with invalid data
result = validate_contact_info(
"not-an-email",
"https://example.com",
"555-123-4567"
)
print(f"{result['status']}: {result['field']} - {result['error']}")
Date and Time Validators
Custom validators for date and time validation:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
from datetime import date, timedelta
# Date range validator
def date_between(start_date, end_date, error_message=None):
def validator(value):
if start_date <= value <= end_date:
return Maybe.success(value)
return Maybe.failure(
error_message or f"Date must be between {start_date} and {end_date}"
)
return validators.Validator(validator)
# Future date validator
def future_date(include_today=True, error_message=None):
def validator(value):
today = date.today()
if include_today and value >= today:
return Maybe.success(value)
elif not include_today and value > today:
return Maybe.success(value)
return Maybe.failure(
error_message or "Date must be in the future"
)
return validators.Validator(validator)
# Past date validator
def past_date(include_today=True, error_message=None):
def validator(value):
today = date.today()
if include_today and value <= today:
return Maybe.success(value)
elif not include_today and value < today:
return Maybe.success(value)
return Maybe.failure(
error_message or "Date must be in the past"
)
return validators.Validator(validator)
# Weekday validator
def is_weekday(error_message=None):
def validator(value):
if value.weekday() < 5: # Monday(0) to Friday(4)
return Maybe.success(value)
return Maybe.failure(
error_message or "Date must be a weekday"
)
return validators.Validator(validator)
# Weekend validator
def is_weekend(error_message=None):
def validator(value):
if value.weekday() >= 5: # Saturday(5) and Sunday(6)
return Maybe.success(value)
return Maybe.failure(
error_message or "Date must be a weekend"
)
return validators.Validator(validator)
# Process date with pattern matching
def validate_appointment_date(appointment_date):
today = date.today()
next_month = today + timedelta(days=30)
# Create validators
is_this_month = date_between(today, next_month)
is_weekday = is_weekday()
# Check if date is valid for appointment
month_result = is_this_month(appointment_date)
weekday_result = is_weekday(appointment_date)
match (month_result, weekday_result):
case (Success(_), Success(_)):
return f"Appointment on {appointment_date.isoformat()} is valid (weekday this month)"
case (Failure(_), Success(_)):
return f"Appointment on {appointment_date.isoformat()} is invalid (not within a month)"
case (Success(_), Failure(_)):
return f"Appointment on {appointment_date.isoformat()} is invalid (not a weekday)"
case (Failure(err1), Failure(err2)):
return f"Appointment on {appointment_date.isoformat()} is invalid: {err1} and {err2}"
# Test with different dates
valid_date = date.today() + timedelta(days=5)
weekend_date = date.today() + timedelta(days=(5 - date.today().weekday() + 6) % 7)
future_date = date.today() + timedelta(days=60)
print(validate_appointment_date(valid_date))
print(validate_appointment_date(weekend_date))
print(validate_appointment_date(future_date))
Collection Validators
Custom validators for collections like lists and dictionaries:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
# List length validator
def list_length(min_length, max_length=None, error_message=None):
if max_length is None:
max_length = float('inf')
def validator(value):
if not isinstance(value, list):
return Maybe.failure("Value must be a list")
if min_length <= len(value) <= max_length:
return Maybe.success(value)
if min_length == max_length:
return Maybe.failure(
error_message or f"List must contain exactly {min_length} items"
)
return Maybe.failure(
error_message or f"List must contain between {min_length} and {max_length} items"
)
return validators.Validator(validator)
# Validator for all list items
def each_item(item_validator, error_message=None):
def validator(value):
if not isinstance(value, list):
return Maybe.failure("Value must be a list")
errors = []
results = []
for i, item in enumerate(value):
result = item_validator(item)
match result:
case Success(validated_item):
results.append(validated_item)
case Failure(error):
errors.append(f"Item {i}: {error}")
if errors:
return Maybe.failure(
error_message or "\n".join(errors)
)
return Maybe.success(results)
return validators.Validator(validator)
# Dictionary validator with required keys
def has_keys(required_keys, error_message=None):
def validator(value):
if not isinstance(value, dict):
return Maybe.failure("Value must be a dictionary")
missing_keys = [key for key in required_keys if key not in value]
if missing_keys:
return Maybe.failure(
error_message or f"Missing required keys: {', '.join(missing_keys)}"
)
return Maybe.success(value)
return validators.Validator(validator)
# Validate a product catalog with pattern matching
def validate_product_catalog(products):
is_non_empty_list = list_length(1)
is_positive_price = validators.minimum(0)
all_valid_prices = each_item(is_positive_price)
has_required_product_fields = has_keys(["name", "price", "stock"])
# First validate that we have a non-empty list
list_result = is_non_empty_list(products)
match list_result:
case Failure(error):
return f"Invalid product catalog: {error}"
case Success(_):
pass # Continue validation
# Then validate each product
valid_products = []
invalid_products = []
for i, product in enumerate(products):
# Validate product structure
structure_result = has_required_product_fields(product)
match structure_result:
case Failure(error):
invalid_products.append({
"index": i,
"product": product,
"error": error
})
continue
case Success(_):
# Validate price
price_result = is_positive_price(product.get("price", 0))
match price_result:
case Failure(error):
invalid_products.append({
"index": i,
"product": product,
"error": f"Price is invalid: {error}"
})
continue
case Success(_):
valid_products.append(product)
if invalid_products:
return {
"status": "partial",
"valid_count": len(valid_products),
"invalid_count": len(invalid_products),
"invalid_products": invalid_products
}
return {
"status": "success",
"product_count": len(valid_products),
"products": valid_products
}
# Test with a product catalog
products = [
{"name": "Laptop", "price": 999.99, "stock": 10},
{"name": "Phone", "price": 499.99, "stock": 20},
{"name": "Headphones", "price": -49.99, "stock": 30}, # Invalid price
{"name": "Tablet", "stock": 15} # Missing price
]
result = validate_product_catalog(products)
print(f"Status: {result['status']}")
if result["status"] == "partial":
print(f"Valid products: {result['valid_count']}")
print(f"Invalid products: {result['invalid_count']}")
for invalid in result["invalid_products"]:
print(f" Product #{invalid['index']}: {invalid['error']}")
Custom Domain-Specific Validators
Creating validators for specific business domains:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
# Credit card validator
def credit_card_validator(error_message=None):
def luhn_check(card_number):
"""Luhn algorithm for credit card validation."""
digits = [int(d) for d in card_number if d.isdigit()]
if not digits or len(digits) < 13 or len(digits) > 19:
return False
# Luhn algorithm
checksum = 0
odd_even = len(digits) % 2
for i, digit in enumerate(digits):
if ((i + odd_even) % 2) == 0:
doubled = digit * 2
checksum += doubled if doubled < 10 else doubled - 9
else:
checksum += digit
return checksum % 10 == 0
def validator(value):
# Remove spaces and dashes
card_number = ''.join(c for c in value if c.isdigit() or c.isalpha())
if luhn_check(card_number):
return Maybe.success(value)
return Maybe.failure(
error_message or "Invalid credit card number"
)
return validators.Validator(validator)
# ISBN validator
def isbn_validator(error_message=None):
def validate_isbn10(isbn):
if len(isbn) != 10:
return False
# ISBN-10 validation
digits = [int(c) if c.isdigit() else 10 if c == 'X' else -1 for c in isbn]
if -1 in digits:
return False
checksum = sum((10 - i) * digit for i, digit in enumerate(digits))
return checksum % 11 == 0
def validate_isbn13(isbn):
if len(isbn) != 13:
return False
# ISBN-13 validation
digits = [int(c) for c in isbn if c.isdigit()]
if len(digits) != 13:
return False
checksum = sum(digit if i % 2 == 0 else digit * 3 for i, digit in enumerate(digits))
return checksum % 10 == 0
def validator(value):
# Remove dashes and spaces
isbn = ''.join(c for c in value if c.isdigit() or c == 'X')
if validate_isbn10(isbn) or validate_isbn13(isbn):
return Maybe.success(value)
return Maybe.failure(
error_message or "Invalid ISBN"
)
return validators.Validator(validator)
# Validate payment and product information
def validate_purchase(credit_card, isbn):
cc_validator = credit_card_validator()
book_validator = isbn_validator()
cc_result = cc_validator(credit_card)
isbn_result = book_validator(isbn)
match (cc_result, isbn_result):
case (Success(cc), Success(book)):
return {
"status": "approved",
"message": "Purchase approved",
"payment": f"xxxx-xxxx-xxxx-{cc[-4:]}",
"product": book
}
case (Failure(error), _):
return {
"status": "declined",
"reason": "payment",
"message": error
}
case (_, Failure(error)):
return {
"status": "declined",
"reason": "product",
"message": error
}
# Test with valid values
purchase_result = validate_purchase(
"4111 1111 1111 1111", # Valid test number
"978-3-16-148410-0" # Valid ISBN-13
)
print(f"Purchase status: {purchase_result['status']}")
# Test with invalid values
purchase_result = validate_purchase(
"4111 1111 1111 1112", # Invalid number
"978-3-16-148410-0" # Valid ISBN-13
)
print(f"Purchase status: {purchase_result['status']}")
print(f"Reason: {purchase_result['reason']}")
print(f"Message: {purchase_result['message']}")
Creating Stateful Validators
Validators that maintain state or depend on external resources:
from valid8r import Maybe, validators
from valid8r.core.maybe import Success, Failure
# Validator that ensures uniqueness within a session
class UniqueValidator:
def __init__(self, error_message=None):
self.seen_values = set()
self.error_message = error_message or "Value must be unique"
def __call__(self, value):
if value in self.seen_values:
return Maybe.failure(self.error_message)
self.seen_values.add(value)
return Maybe.success(value)
def reset(self):
self.seen_values.clear()
# Use stateful validators with pattern matching
def register_usernames(usernames):
is_unique = UniqueValidator("This username has already been registered")
registered = []
errors = []
for i, username in enumerate(usernames):
result = is_unique(username)
match result:
case Success(value):
registered.append(value)
case Failure(error):
errors.append({"index": i, "username": username, "error": error})
return {
"registered": registered,
"errors": errors,
"success_count": len(registered),
"error_count": len(errors)
}
# Test with a list of usernames
result = register_usernames(["alice", "bob", "charlie", "alice", "david", "bob"])
print(f"Registered {result['success_count']} users:")
for user in result["registered"]:
print(f" - {user}")
print(f"Encountered {result['error_count']} errors:")
for error in result["errors"]:
print(f" - {error['username']}: {error['error']}")
In the next section, we’ll explore interactive prompting techniques with validation.