Valid8r is a Python validation library with a unique focus on CLI applications, network data parsing, and functional programming patterns. This guide helps you choose the right validation library for your use case.
| Use Case | Choose This Library |
|---|---|
| Building a FastAPI REST API | Pydantic - Best-in-class FastAPI integration, auto-generated OpenAPI docs |
| Performance-critical data validation (millions of records) | Pydantic - Rust-powered core, 5-50x faster than alternatives |
| Flask API with explicit serialization | marshmallow - Excellent Flask ecosystem, separate serialize/deserialize |
| Lightweight validation with zero dependencies | cerberus - No dependencies, simple dict validation |
| CLI applications with user prompts | valid8r - Interactive input, automatic retry, rich error messages |
| Parsing network data (URLs, emails, IPs, phones) | valid8r - Returns structured dataclasses (UrlParts, EmailAddress, etc.) |
| Functional programming patterns (Maybe monad) | valid8r - Railway-oriented programming, no exceptions |
| Environment variable configuration | valid8r - Built-in env schema with validation |
| TypeScript-style validation | Pydantic - Similar ergonomics, type annotations |
| Legacy Python codebases (Flask, SQLAlchemy) | marshmallow - Proven track record, stable API |
| Feature | valid8r | Pydantic | marshmallow | cerberus |
|---|---|---|---|---|
| Performance | Good (see benchmarks) | Good (Rust core) | Moderate | Good |
| Dependencies | pydantic, email-validator | pydantic-core (Rust) | None (core) | None |
| Python Versions | 3.11+ | 3.8+ | 3.8+ | 3.7+ |
| Error Handling | Maybe monad | Exceptions | Exceptions | Non-blocking |
| Structured Network Results | ⭐ Yes (dataclasses) | No | No | No |
| CLI Integration | ⭐ Built-in prompts | No | No | No |
| FastAPI Integration | Via Pydantic | ⭐ Native | Via plugin | No |
| JSON Schema | No | ⭐ Yes | Yes | No |
| Interactive Validation | ⭐ Yes (retry logic) | No | No | No |
| Functional Composition | ⭐ Yes (&, |, ~) | Limited | No | No |
| Type Annotations | Full | ⭐ Full | Partial | Schema-based |
| Learning Curve | Moderate | Moderate | Low | Low |
| Ecosystem Size | Small | ⭐ Very Large | Large | Medium |
Choose Pydantic if:
- Building FastAPI or Django Ninja APIs
- Need automatic OpenAPI/JSON Schema generation
- Processing high-volume data (millions of records)
- Want the largest ecosystem and community
- Need seamless ORM integration (SQLModel, etc.)
Choose valid8r if:
- Building CLI applications with interactive prompts
- Parsing network data (URLs, emails, IPs) into structured types
- Prefer functional programming (Maybe monad, no exceptions)
- Need built-in input retry logic with validation
- Want lightweight environment variable configuration
Example Comparison:
Pydantic Approach:
from pydantic import BaseModel, EmailStr, HttpUrl, field_validator
class User(BaseModel):
email: EmailStr
website: HttpUrl
age: int
@field_validator('age')
@classmethod
def validate_age(cls, v):
if not 0 <= v <= 120:
raise ValueError('Age must be between 0 and 120')
return v
# Usage - raises exceptions on failure
try:
user = User(email="user@example.com", website="https://example.com", age=25)
print(user.email) # Returns string: "user@example.com"
except ValidationError as e:
print(e.errors())valid8r Approach:
from valid8r import parsers, validators, prompt
from valid8r.core.maybe import Success, Failure
# Interactive CLI with automatic retry
email_result = prompt.ask(
"Email: ",
parser=parsers.parse_email,
retry=2
)
match email_result:
case Success(email):
# Returns structured EmailAddress(local='user', domain='example.com')
print(f"{email.local}@{email.domain}")
print(f"Domain is normalized: {email.domain}") # Lowercase
case Failure(error):
print(f"Error: {error}")
# URL parsing returns structured components
url_result = parsers.parse_url("https://user:pass@example.com:8443/path?q=1#frag")
match url_result:
case Success(url):
# UrlParts dataclass with all components parsed
print(url.scheme) # 'https'
print(url.host) # 'example.com'
print(url.port) # 8443
print(url.path) # '/path'
print(url.query) # {'q': '1'}
print(url.fragment) # 'frag'Key Differences:
-
Error Handling Philosophy:
- Pydantic: Exceptions (try/except required)
- valid8r: Maybe monad (pattern matching or
.is_success())
-
Network Parsing:
- Pydantic: Returns strings (
EmailStris still a string) - valid8r: Returns structured dataclasses (EmailAddress, UrlParts, PhoneNumber)
- Pydantic: Returns strings (
-
Interactive Input:
- Pydantic: Not designed for user prompts
- valid8r: Built-in
prompt.ask()with retry logic
-
Performance:
- Both libraries are fast enough for most use cases
- Benchmark results vary by operation (see performance benchmarks)
- For high-throughput APIs, profile your specific workload
When to Use Both:
You can use Pydantic for FastAPI models and valid8r for CLI commands:
from fastapi import FastAPI
from pydantic import BaseModel
from valid8r import parsers
from valid8r.integrations import validator_from_parser
app = FastAPI()
class UserCreate(BaseModel):
email: str
@field_validator('email', mode='before')
@classmethod
def validate_email(cls, v):
# Use valid8r's email parser in Pydantic model
return validator_from_parser(parsers.parse_email)(v)Choose marshmallow if:
- Building Flask APIs
- Need explicit control over serialization/deserialization
- Working with SQLAlchemy models
- Want zero dependencies
- Prefer schema-based validation over type annotations
Choose valid8r if:
- Building CLI applications
- Need structured network parsing (URLs, emails, IPs)
- Prefer functional composition over schema definitions
- Want Maybe monad error handling
- Need interactive input prompting
Example Comparison:
marshmallow Approach:
from marshmallow import Schema, fields, validate, ValidationError
class UserSchema(Schema):
email = fields.Email(required=True)
age = fields.Integer(required=True, validate=validate.Range(min=0, max=120))
website = fields.URL(required=True)
schema = UserSchema()
# Explicit deserialization
try:
result = schema.load({'email': 'user@example.com', 'age': 25, 'website': 'https://example.com'})
print(result['email']) # Returns dict: {'email': '...', 'age': 25, ...}
except ValidationError as e:
print(e.messages)
# Explicit serialization
output = schema.dump(result)valid8r Approach:
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Functional composition with Maybe monad
age_parser = lambda x: parsers.parse_int(x).bind(validators.between(0, 120))
email_result = parsers.parse_email("user@example.com")
age_result = age_parser("25")
url_result = parsers.parse_url("https://example.com")
# Pattern matching for error handling
match (email_result, age_result, url_result):
case (Success(email), Success(age), Success(url)):
print(f"Email: {email.local}@{email.domain}")
print(f"Age: {age}")
print(f"URL: {url.scheme}://{url.host}")
case _:
# Handle any failures
for result in [email_result, age_result, url_result]:
if result.is_failure():
print(result.error_or("Unknown error"))Key Differences:
-
API Style:
- marshmallow: Schema classes, explicit load/dump
- valid8r: Functional composition, Maybe monad
-
Serialization:
- marshmallow: Bidirectional (serialize/deserialize)
- valid8r: Focused on parsing/validation (deserialization only)
-
Type Safety:
- marshmallow: Runtime schema validation
- valid8r: Type hints + runtime validation
-
Error Handling:
- marshmallow: ValidationError exceptions
- valid8r: Maybe monad (no exceptions)
Migration Guide: marshmallow → valid8r
# Before (marshmallow)
from marshmallow import Schema, fields, validate
class ConfigSchema(Schema):
port = fields.Integer(validate=validate.Range(min=1024, max=65535))
debug = fields.Boolean()
email = fields.Email(required=True)
schema = ConfigSchema()
config = schema.load(data)
# After (valid8r)
from valid8r.integrations.env import EnvSchema, EnvField
from valid8r import parsers, validators
schema = EnvSchema(fields={
'port': EnvField(
parser=lambda x: parsers.parse_int(x).bind(validators.between(1024, 65535)),
default=8080
),
'debug': EnvField(parser=parsers.parse_bool, default=False),
'email': EnvField(parser=parsers.parse_email, required=True),
})
result = load_env_config(schema, prefix='APP_')
match result:
case Success(config):
# Typed configuration
port = config['port'] # int, validatedChoose cerberus if:
- Need zero dependencies
- Validating simple dictionaries/JSON
- Want lightweight validation
- Building Eve framework applications
- Prefer non-blocking validation (collect all errors)
Choose valid8r if:
- Building CLI applications with user interaction
- Need structured network parsing
- Want functional composition
- Prefer type annotations over schema dicts
- Need Maybe monad error handling
Example Comparison:
cerberus Approach:
from cerberus import Validator
schema = {
'email': {'type': 'string', 'regex': r'^[^@]+@[^@]+\.[^@]+$'},
'age': {'type': 'integer', 'min': 0, 'max': 120},
'website': {'type': 'string', 'regex': r'^https?://'}
}
validator = Validator(schema)
data = {'email': 'user@example.com', 'age': 25, 'website': 'https://example.com'}
if validator.validate(data):
print("Valid!")
print(data['email']) # Returns raw string
else:
print(validator.errors) # {'age': ['min value is 0']}valid8r Approach:
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Type-safe parsing with structured results
email = parsers.parse_email("user@example.com")
age = parsers.parse_int("25").bind(validators.between(0, 120))
url = parsers.parse_url("https://example.com")
match (email, age, url):
case (Success(e), Success(a), Success(u)):
# Structured data types
print(f"Email domain: {e.domain}") # EmailAddress dataclass
print(f"Age: {a}") # int
print(f"URL scheme: {u.scheme}") # UrlParts dataclassKey Differences:
-
Dependencies:
- cerberus: Zero dependencies
- valid8r: Requires pydantic and email-validator (~5MB install size)
-
Validation Style:
- cerberus: Dict-based schemas, non-blocking validation
- valid8r: Functional composition, fails fast
-
Type Safety:
- cerberus: No type hints (runtime schema)
- valid8r: Full type annotations
-
Network Parsing:
- cerberus: Regex validation only
- valid8r: Structured parsing (EmailAddress, UrlParts, etc.)
Migration Guide: cerberus → valid8r
# Before (cerberus)
from cerberus import Validator
schema = {
'name': {'type': 'string', 'minlength': 3, 'maxlength': 128},
'age': {'type': 'integer', 'min': 0, 'max': 120},
'tags': {'type': 'list', 'schema': {'type': 'string'}}
}
v = Validator(schema)
if v.validate(data):
process(data)
# After (valid8r)
from valid8r import parsers, validators
name = parsers.parse_str(data['name']).bind(validators.length(3, 128))
age = parsers.parse_int(data['age']).bind(validators.between(0, 120))
tags = parsers.parse_list(data['tags'], element_parser=parsers.parse_str)
results = [name, age, tags]
if all(r.is_success() for r in results):
process({
'name': name.value_or(''),
'age': age.value_or(0),
'tags': tags.value_or([])
})Disclaimer: Benchmarks vary significantly by operation type and use case. Always profile your specific workload.
Performance comparisons between validation libraries are nuanced:
- Simple type parsing (int, float, bool): valid8r's minimal abstraction provides fast parsing
- Complex models: Performance depends on validation complexity and features used
- Email/URL validation: valid8r uses comprehensive validation (email-validator with DNS checks), while simpler regex validation is faster but less thorough
See docs/performance.md for detailed benchmarks with methodology.
All libraries are fast enough for:
- User input validation in web forms
- CLI applications (human interaction speed)
- Moderate-throughput APIs (<1000 req/sec)
Profile before optimizing: Unless validation shows up in profiling as a bottleneck, choose the library that best fits your use case and development style rather than optimizing for benchmarks.
When to migrate:
- Moving from FastAPI to CLI application
- Need structured network parsing
- Prefer functional programming patterns
Migration steps:
# Before (Pydantic)
from pydantic import BaseModel, EmailStr, field_validator
class Config(BaseModel):
email: EmailStr
port: int
@field_validator('port')
@classmethod
def validate_port(cls, v):
if not 1024 <= v <= 65535:
raise ValueError('Port must be 1024-65535')
return v
try:
config = Config(email="admin@example.com", port=8080)
except ValidationError as e:
print(e.errors())
# After (valid8r)
from valid8r import parsers, validators
from valid8r.integrations.env import EnvSchema, EnvField, load_env_config
schema = EnvSchema(fields={
'email': EnvField(parser=parsers.parse_email, required=True),
'port': EnvField(
parser=lambda x: parsers.parse_int(x).bind(validators.between(1024, 65535)),
required=True
),
})
result = load_env_config(schema, prefix='APP_')
match result:
case Success(config):
email = config['email'] # EmailAddress dataclass
port = config['port'] # int
case Failure(error):
print(error)Key changes:
- Replace
BaseModelwithEnvSchemafor config - Replace
@field_validatorwithlambda x: parser(x).bind(validator(...)) - Replace
try/exceptwithmatchpattern matching - Use
.local/.domainfor EmailAddress instead of string
When to migrate:
- Moving from Flask to Click CLI
- Need structured network parsing
- Want functional composition
Migration steps:
# Before (marshmallow)
from marshmallow import Schema, fields, validate, ValidationError
class UserSchema(Schema):
email = fields.Email(required=True)
age = fields.Integer(validate=validate.Range(min=0, max=120))
schema = UserSchema()
try:
result = schema.load({'email': 'user@example.com', 'age': 25})
except ValidationError as e:
print(e.messages)
# After (valid8r)
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
email = parsers.parse_email("user@example.com")
age = parsers.parse_int("25").bind(validators.between(0, 120))
match (email, age):
case (Success(e), Success(a)):
print(f"Email: {e.local}@{e.domain}")
print(f"Age: {a}")
case _:
errors = [r.error_or("") for r in [email, age] if r.is_failure()]
print(errors)Key changes:
- Replace
Schemaclasses with functional composition - Replace
fields.X()withparsers.parse_X() - Replace
validate=validate.Range()with.bind(validators.between()) - Replace
try/exceptwithmatchpattern matching - Use structured types (EmailAddress) instead of strings
When to migrate:
- Need type annotations
- Want structured network parsing
- Prefer functional style
Migration steps:
# Before (cerberus)
from cerberus import Validator
schema = {
'email': {'type': 'string', 'regex': r'^[^@]+@[^@]+\.[^@]+$'},
'age': {'type': 'integer', 'min': 0, 'max': 120}
}
v = Validator(schema)
if v.validate(data):
print(data['email'])
else:
print(v.errors)
# After (valid8r)
from valid8r import parsers, validators
email = parsers.parse_email(data['email'])
age = parsers.parse_int(data['age']).bind(validators.between(0, 120))
if email.is_success() and age.is_success():
print(f"Email: {email.value_or('')}")
print(f"Age: {age.value_or(0)}")
else:
print([r.error_or("") for r in [email, age] if r.is_failure()])Key changes:
- Replace dict schemas with function calls
- Replace
{'type': 'integer'}withparsers.parse_int() - Replace
{'min': X, 'max': Y}with.bind(validators.between(X, Y)) - Replace
v.validate()with.is_success() - Use structured types instead of validated dicts
Use Case: FastAPI backend + CLI management tool
# api.py - FastAPI with Pydantic
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class User(BaseModel):
email: EmailStr
age: int
@app.post("/users")
def create_user(user: User):
return {"email": user.email, "age": user.age}
# cli.py - Click CLI with valid8r
import click
from valid8r import parsers, prompt, validators
from valid8r.integrations import ParamTypeAdapter
@click.command()
def create_user_interactive():
"""Create user via interactive CLI."""
email = prompt.ask(
"Email: ",
parser=parsers.parse_email,
retry=2
)
age = prompt.ask(
"Age: ",
parser=parsers.parse_int,
validator=validators.between(0, 120),
retry=2
)
match (email, age):
case (Success(e), Success(a)):
# Call API with validated data
create_user(User(email=f"{e.local}@{e.domain}", age=a))Rationale: Pydantic excels at API validation, valid8r excels at CLI interaction. Use both where each is strongest.
from pydantic import BaseModel, field_validator
from valid8r import parsers, validators
from valid8r.integrations import validator_from_parser
class Config(BaseModel):
port: int
email: str
@field_validator('port', mode='before')
@classmethod
def validate_port(cls, v):
# Use valid8r's parser + validator in Pydantic
parser = lambda x: parsers.parse_int(x).bind(validators.between(1024, 65535))
return validator_from_parser(parser)(v)
@field_validator('email', mode='before')
@classmethod
def validate_email(cls, v):
# Use valid8r's structured email parsing
return validator_from_parser(parsers.parse_email)(v)Valid8r fills specific gaps that existing libraries don't address:
- Interactive CLI validation: No other library provides built-in prompting with retry logic
- Structured network parsing: Pydantic/marshmallow/cerberus return strings; valid8r returns dataclasses (EmailAddress, UrlParts, PhoneNumber)
- Functional composition: Maybe monad enables railway-oriented programming without exceptions
- Environment variables: Built-in schema for typed, validated env config
Valid8r is not trying to replace Pydantic for APIs or marshmallow for Flask. It's designed for CLI applications and network data parsing.
It depends on the operation. Both libraries have different performance characteristics.
See docs/performance.md for detailed benchmarks. Key points:
- Both libraries are fast enough for typical validation use cases
- Performance varies by operation type (simple parsing vs. complex models)
- For most applications, choose based on features and API preference, not benchmarks
- If validation is a bottleneck in profiling, test both libraries with your specific workload
For CLI apps, the difference is negligible (human interaction is the bottleneck).
Yes, but Pydantic is better suited for FastAPI.
FastAPI is designed around Pydantic:
- Automatic OpenAPI docs from Pydantic models
- Native integration with request/response validation
- JSON Schema generation
You can use valid8r parsers in Pydantic models via validator_from_parser():
from fastapi import FastAPI
from pydantic import BaseModel, field_validator
from valid8r import parsers
from valid8r.integrations import validator_from_parser
app = FastAPI()
class User(BaseModel):
email: str
@field_validator('email', mode='before')
@classmethod
def validate_email(cls, v):
return validator_from_parser(parsers.parse_email)(v)Recommendation: Use Pydantic for FastAPI, use valid8r for CLI tools.
Three unique features:
-
Structured Network Parsing
- Other libraries:
email: str = "user@example.com" - valid8r:
email: EmailAddress(local='user', domain='example.com')
- Other libraries:
-
CLI-First Design
- Built-in
prompt.ask()with retry logic - Rich error messages optimized for terminal output
- Click integration via
ParamTypeAdapter
- Built-in
-
Maybe Monad Error Handling
- No exceptions in validation path
- Composable via
.bind()and.map() - Pattern matching for error handling
Example:
from valid8r import parsers
# Returns UrlParts dataclass, not string
url = parsers.parse_url("https://user:pass@example.com:8443/path?q=1#frag")
print(url.value_or(None).scheme) # 'https'
print(url.value_or(None).port) # 8443
print(url.value_or(None).query) # {'q': '1'}Probably not, unless:
- You're building a CLI app (use valid8r instead)
- You need structured network parsing (use valid8r for parsing, Pydantic for models)
- You prefer functional programming (Maybe monad vs exceptions)
Don't migrate if:
- Building FastAPI/Django Ninja APIs (Pydantic is superior)
- Need JSON Schema or OpenAPI generation (Pydantic only)
- Performance is critical (Pydantic is much faster)
Hybrid approach (recommended):
- Use Pydantic for web APIs
- Use valid8r for CLI tools and network parsing
- Use valid8r parsers inside Pydantic validators when needed
Yes. Valid8r provides comprehensive async validation support for I/O-bound operations like database checks, API calls, and external service validation.
Key features:
- Concurrent execution of async validators across fields
- Mixed sync and async validators (sync runs first for fail-fast behavior)
- Configurable timeout support
- Full error accumulation
See the Async Validation Guide for details.
Yes. Use make_parser or create_parser:
from valid8r.core.parsers import make_parser
from valid8r.core.maybe import Maybe
@make_parser
def parse_hex_color(text: str) -> Maybe[str]:
"""Parse hex color code (e.g., #FF5733)."""
if not text.startswith('#'):
return Maybe.failure('Color must start with #')
if len(text) != 7:
return Maybe.failure('Color must be #RRGGBB format')
try:
int(text[1:], 16)
return Maybe.success(text.upper())
except ValueError:
return Maybe.failure('Invalid hex color')
# Use like any other parser
result = parse_hex_color("#ff5733")Yes.
- Stable API (v1.27.0+, semantic versioning)
- Comprehensive test coverage (>95%)
- Type-safe (passes strict mypy)
- Security: DoS protection, input length limits
- Actively maintained
Dependencies: Valid8r requires Pydantic and email-validator as runtime dependencies (adds ~5MB to install size).
Production deployments: See Security Guide for rate limiting, input size limits, and defense-in-depth strategies.
Beginner-friendly if you understand:
- Type hints
- Pattern matching (Python 3.10+)
- Basic functional programming (map, bind)
Steeper if new to:
- Maybe monad pattern
- Railway-oriented programming
- Functional composition
Time to productivity:
- Basic parsing: 15 minutes
- Validators and composition: 1 hour
- Advanced parsers and integrations: 2-4 hours
| Your Situation | Recommended Library |
|---|---|
| Building REST API with FastAPI | Pydantic |
| Building Flask API with SQLAlchemy | marshmallow |
| Building CLI tool with user prompts | valid8r |
| Zero dependencies required | cerberus |
| Parsing URLs/emails into structured data | valid8r |
| Performance-critical bulk validation | Pydantic |
| Functional programming patterns | valid8r |
| Need JSON Schema / OpenAPI | Pydantic |
| Environment variable configuration | valid8r |
| Legacy Python 3.7 support | cerberus or marshmallow |
The Bottom Line:
- Pydantic is the industry standard for web APIs and data modeling
- marshmallow is battle-tested for Flask and SQLAlchemy
- cerberus is the lightweight choice for simple dict validation
- valid8r is purpose-built for CLI apps and network data parsing
Valid8r's niche: If you're building CLI applications or need structured network parsing, valid8r provides features that no other library offers. For everything else, consider the established alternatives first.
Community: