Clean, composable input validation for Python using functional programming patterns.
Valid8r makes input validation elegant and type-safe by using the Maybe monad for error handling. No more try-except blocks or boolean validation chains—just clean, composable parsers that tell you exactly what went wrong.
from valid8r import parsers, validators, prompt
# Parse and validate user input with rich error messages
age = prompt.ask(
"Enter your age: ",
parser=parsers.parse_int,
validator=validators.minimum(0) & validators.maximum(120)
)
print(f"Your age is {age}")Type-Safe Parsing: Every parser returns Maybe[T] (Success or Failure), making error handling explicit and composable.
Rich Structured Results: Network parsers return dataclasses with parsed components—no more manual URL/email splitting.
Chainable Validators: Combine validators using & (and), | (or), and ~ (not) operators for complex validation logic.
Security-First Design: All parsers include DoS protection via input length validation and automated ReDoS detection prevents vulnerable regex patterns.
Framework Integrations: Built-in support for Pydantic (always included) and optional Click integration for CLI apps.
Interactive Prompts: Built-in user input prompting with automatic retry and validation.
High Performance: Valid8r is 4-300x faster than Pydantic for basic parsing, making it ideal for high-throughput APIs and batch processing.
Valid8r is designed for high-performance validation with minimal overhead:
| Scenario | valid8r | Pydantic | Speedup |
|---|---|---|---|
| Integer parsing | 375ns | 102µs | 273x faster |
| Nested objects | 37µs | 568µs | 15x faster |
| List (100 items) | 30µs | 126µs | 4x faster |
When to use valid8r:
- High-throughput APIs (>5K requests/sec)
- Batch processing pipelines
- CLI tools requiring structured parsing
- Performance-critical code paths
When to use Pydantic:
- FastAPI applications (tight integration)
- Complex data models with auto-schema generation
- Developer experience > raw performance
Full benchmarks and methodology: docs/performance.md
Basic installation (includes Pydantic integration):
pip install valid8rWith optional framework integrations:
# Click integration for CLI applications
pip install 'valid8r[click]'
# All optional integrations
pip install 'valid8r[click]'Requirements: Python 3.11 or higher
| Feature | Installation | Import |
|---|---|---|
| Core parsers & validators | pip install valid8r |
from valid8r import parsers, validators |
| Pydantic integration | included by default | from valid8r.integrations import validator_from_parser |
| Click integration (CLI) | pip install 'valid8r[click]' |
from valid8r.integrations import ParamTypeAdapter |
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
# Parse integers with automatic error handling
match parsers.parse_int("42"):
case Success(value):
print(f"Parsed: {value}") # Parsed: 42
case Failure(error):
print(f"Error: {error}")
# Parse dates (ISO 8601 format)
result = parsers.parse_date("2025-01-15")
assert result.is_success()
# Parse UUIDs with version validation
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000", version=4)
assert result.is_success()from valid8r import parsers
from datetime import UTC
# Parse timezone-aware datetime (ISO 8601)
result = parsers.parse_datetime("2024-01-15T10:30:00Z")
match result:
case Success(dt):
print(f"DateTime: {dt}") # 2024-01-15 10:30:00+00:00
print(f"Timezone: {dt.tzinfo}") # UTC
case Failure(error):
print(f"Error: {error}")
# Parse with timezone offset
result = parsers.parse_datetime("2024-01-15T10:30:00+05:30")
assert result.is_success()
# Parse duration/timedelta in multiple formats
result = parsers.parse_timedelta("1h 30m") # Simple format
assert result.value_or(None).total_seconds() == 5400
result = parsers.parse_timedelta("PT1H30M") # ISO 8601 duration
assert result.value_or(None).total_seconds() == 5400from valid8r import validators
# Combine validators using operators
age_validator = validators.minimum(0) & validators.maximum(120)
result = age_validator(42)
assert result.is_success()
# String validation
password_validator = (
validators.length(8, 128) &
validators.matches_regex(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]')
)
# Set validation
tags_validator = validators.subset_of({'python', 'rust', 'go', 'typescript'})from valid8r import parsers
# Parse URLs into structured components
match parsers.parse_url("https://user:[email protected]:8443/path?query=1#fragment"):
case Success(url):
print(f"Scheme: {url.scheme}") # https
print(f"Host: {url.host}") # example.com
print(f"Port: {url.port}") # 8443
print(f"Path: {url.path}") # /path
print(f"Query: {url.query}") # {'query': '1'}
print(f"Fragment: {url.fragment}") # fragment
# Parse emails with normalized domains
match parsers.parse_email("[email protected]"):
case Success(email):
print(f"Local: {email.local}") # User
print(f"Domain: {email.domain}") # example.com (normalized)
# Parse phone numbers (NANP format)
match parsers.parse_phone("+1 (415) 555-2671"):
case Success(phone):
print(f"E.164: {phone.e164}") # +14155552671
print(f"National: {phone.national}") # (415) 555-2671from valid8r import parsers
# Parse lists with element validation
result = parsers.parse_list("1,2,3,4,5", element_parser=parsers.parse_int)
assert result.value_or([]) == [1, 2, 3, 4, 5]
# Parse dictionaries with key/value parsers
result = parsers.parse_dict(
"name=Alice,age=30",
key_parser=lambda x: Success(x),
value_parser=lambda x: parsers.parse_int(x) if x.isdigit() else Success(x)
)Automatically generate parsers from Python type annotations:
from typing import Annotated, Literal, Optional
from valid8r.core.type_adapters import from_type
from valid8r import validators
# Generate parser from type annotation
parser = from_type(int)
result = parser('42')
assert result.value_or(None) == 42
# Optional types handle None automatically
parser = from_type(Optional[int])
assert parser('').value_or('not none') is None # Empty string becomes None
assert parser('42').value_or(None) == 42
# Collections with element validation
parser = from_type(list[int])
result = parser('[1, 2, 3, 4, 5]')
assert result.value_or([]) == [1, 2, 3, 4, 5]
# Nested structures
parser = from_type(dict[str, list[int]])
result = parser('{"scores": [95, 87, 92]}')
assert result.value_or({}) == {'scores': [95, 87, 92]}
# Combine types with validators using Annotated
Age = Annotated[int, validators.minimum(0), validators.maximum(120)]
parser = from_type(Age)
assert parser('25').value_or(None) == 25
assert parser('150').is_failure() # Exceeds maximum
# Literal types for restricted values
parser = from_type(Literal['red', 'green', 'blue'])
assert parser('red').value_or(None) == 'red'
assert parser('yellow').is_failure() # Not in literal setSecurity: All collection parsers include automatic DoS protection—inputs exceeding 100KB are rejected in <10ms before expensive JSON parsing.
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Parse and validate file paths
match parsers.parse_path("/etc/hosts").bind(validators.exists()).bind(validators.is_file()):
case Success(path):
print(f"Valid file: {path}")
case Failure(err):
print(f"Error: {err}")
# Validate uploaded files
def validate_upload(file_path: str):
return (
parsers.parse_path(file_path)
.bind(validators.exists())
.bind(validators.is_file())
.bind(validators.has_extension(['.pdf', '.docx']))
.bind(validators.max_size(10 * 1024 * 1024)) # 10MB limit
)
# Path expansion and resolution
match parsers.parse_path("~/Documents", expand_user=True):
case Success(path):
print(f"Expanded: {path}") # /Users/username/Documents
match parsers.parse_path("./data/file.txt", resolve=True):
case Success(path):
print(f"Absolute: {path}") # /full/path/to/data/file.txtfrom valid8r import prompt, parsers, validators
# Prompt with validation and automatic retry
email = prompt.ask(
"Email address: ",
parser=parsers.parse_email,
retry=2 # Retry twice on invalid input
)
# Combine parsing and validation
port = prompt.ask(
"Server port: ",
parser=parsers.parse_int,
validator=validators.between(1024, 65535),
retry=3
)Valid8r provides machine-readable error codes and structured error information for programmatic error handling and API responses:
from valid8r import parsers
from valid8r.core.maybe import Success, Failure
from valid8r.core.errors import ErrorCode
# Programmatic error handling using error codes
def process_email(email_str: str):
result = parsers.parse_email(email_str)
match result:
case Success(email):
return f"Valid: {email.local}@{email.domain}"
case Failure():
# Access structured error for programmatic handling
detail = result.error_detail()
# Switch on error codes for different handling
match detail.code:
case ErrorCode.INVALID_EMAIL:
return "Please enter a valid email address"
case ErrorCode.EMPTY_STRING:
return "Email is required"
case ErrorCode.INPUT_TOO_LONG:
return "Email is too long"
case _:
return f"Error: {detail.message}"
# Convert errors to JSON for API responses
result = parsers.parse_int("not-a-number")
match result:
case Failure():
error_dict = result.error_detail().to_dict()
# {
# 'code': 'INVALID_TYPE',
# 'message': 'Input must be a valid integer',
# 'path': '',
# 'context': {}
# }Features:
- Error codes for programmatic handling (e.g.,
ErrorCode.INVALID_EMAIL,ErrorCode.OUT_OF_RANGE) - JSON serialization for API responses via
to_dict() - Field paths for multi-field validation (e.g.,
.user.email) - Debugging context with validation parameters
- 100% backward compatible with string errors
See the Error Handling Guide for comprehensive examples and best practices.
Validate data using async validators for I/O-bound operations like database checks, API calls, and external service validation:
import asyncio
from valid8r.core import parsers, schema, validators
from valid8r.core.maybe import Success, Failure
# Define async validator
async def check_email_unique(email: str) -> Maybe[str]:
"""Check if email is unique in database."""
# Simulate database query
await asyncio.sleep(0.1)
existing_emails = {'[email protected]', '[email protected]'}
if email in existing_emails:
return Maybe.failure('Email already registered')
return Maybe.success(email)
# Create schema with async validators
user_schema = schema.Schema(fields={
'email': schema.Field(
parser=parsers.parse_email,
validators=[
validators.min_length(1), # Sync validator (fail-fast)
check_email_unique, # Async validator (database check)
],
required=True
),
'username': schema.Field(
parser=parsers.parse_str,
validators=[
validators.matches_pattern(r'^[a-z0-9_]+$'),
check_username_available, # Another async validator
],
required=True
),
})
# Validate asynchronously with timeout
async def main():
result = await user_schema.validate_async(
{'email': '[email protected]', 'username': 'newuser'},
timeout=5.0
)
match result:
case Success(data):
print(f"Valid: {data}")
case Failure(errors):
for error in errors:
print(f"{error.path}: {error.message}")
asyncio.run(main())Key Features:
- Concurrent execution of async validators across fields for better performance
- Mixed sync and async validators (sync runs first for fail-fast behavior)
- Configurable timeout support to prevent hanging on slow operations
- Full error accumulation across all fields
- Works seamlessly with existing sync validators
Common Use Cases:
- Database uniqueness checks (email, username)
- External API validation (API keys, payment methods)
- Geolocation constraints (IP address country verification)
- Remote file access validation
- Any I/O-bound validation operation
See the Async Validation Guide for comprehensive examples including database integration, API validation, and performance optimization patterns.
Load typed, validated configuration from environment variables following 12-factor app principles:
from valid8r.integrations.env import EnvSchema, EnvField, load_env_config
from valid8r import parsers, validators
from valid8r.core.maybe import Success, Failure
# Define configuration schema
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),
'database_url': EnvField(parser=parsers.parse_str, required=True),
'admin_email': EnvField(parser=parsers.parse_email, required=True),
})
# Load and validate configuration
result = load_env_config(schema, prefix='APP_')
match result:
case Success(config):
# All values are typed and validated
port = config['port'] # int (validated 1024-65535)
debug = config['debug'] # bool (not str!)
db = config['database_url'] # str (required, guaranteed present)
email = config['admin_email'] # EmailAddress (validated format)
case Failure(error):
print(f"Configuration error: {error}")Features:
- Type-safe parsing (no more string-to-int conversions)
- Declarative validation with composable parsers
- Required vs optional fields with sensible defaults
- Nested schemas for hierarchical configuration
- Clear error messages for missing or invalid values
See Environment Variables Guide for complete examples including FastAPI, Docker, and Kubernetes deployment patterns.
Convert valid8r parsers into Pydantic field validators:
from pydantic import BaseModel, field_validator
from valid8r import parsers, validators
from valid8r.integrations import validator_from_parser
class User(BaseModel):
age: int
@field_validator('age', mode='before')
@classmethod
def validate_age(cls, v):
# Parse string to int, then validate 0-120 range
age_parser = lambda x: parsers.parse_int(x).bind(
validators.between(0, 120)
)
return validator_from_parser(age_parser)(v)
user = User(age="25") # Accepts string, validates, returns intWorks seamlessly with nested models, lists, and complex Pydantic schemas. See Pydantic Integration Examples.
Install: pip install 'valid8r[click]'
Integrate valid8r parsers into Click CLI applications:
import click
from valid8r import parsers
from valid8r.integrations import ParamTypeAdapter
@click.command()
@click.option('--email', type=ParamTypeAdapter(parsers.parse_email))
def send_mail(email):
"""Send an email with validated address."""
click.echo(f"Sending to {email.local}@{email.domain}")
# Automatically validates email format and provides rich error messagesSee Click Integration Examples.
Basic Types:
parse_int,parse_float,parse_bool,parse_decimal,parse_complexparse_date(ISO 8601),parse_uuid(with version validation)
Collections:
parse_list,parse_dict,parse_set(with element parsers)
Network & Communication:
parse_ipv4,parse_ipv6,parse_ip,parse_cidrparse_url→UrlParts(structured URL components)parse_email→EmailAddress(normalized domain)parse_phone→PhoneNumber(NANP validation with E.164 formatting)
Filesystem:
parse_path→pathlib.Path(with expansion and resolution options)
Advanced:
parse_enum(type-safe enum parsing)create_parser,make_parser,validated_parser(custom parser factories)
Numeric: minimum, maximum, between
String: non_empty_string, matches_regex, length
Collection: in_set, unique_items, subset_of, superset_of, is_sorted
Filesystem: exists, is_file, is_dir, is_readable, is_writable, is_executable, max_size, min_size, has_extension
Custom: predicate (create validators from any function)
Combinators: Combine validators using & (and), | (or), ~ (not)
from valid8r.testing import (
assert_maybe_success,
assert_maybe_failure,
MockInputContext,
)
# Test validation logic
result = validators.minimum(0)(42)
assert assert_maybe_success(result, 42)
result = validators.minimum(0)(-5)
assert assert_maybe_failure(result, "at least 0")
# Mock user input for testing prompts
with MockInputContext(["invalid", "[email protected]"]):
result = prompt.ask("Email: ", parser=parsers.parse_email, retry=1)
assert result.is_success()Full documentation: valid8r.readthedocs.io
- Library Comparison Guide - When to choose valid8r vs Pydantic/marshmallow/cerberus
- API Reference
- Parser Guide
- Validator Guide
- Testing Guide
Please do not report security vulnerabilities through public GitHub issues.
Report security issues privately to [email protected] or via GitHub Security Advisories.
See SECURITY.md for our complete security policy, supported versions, and response timeline.
Valid8r is designed for parsing trusted user input in web applications. For production deployments:
- Enforce input size limits at the framework level (recommended: 10KB max request size)
- Implement rate limiting for validation endpoints (recommended: 10 requests/minute)
- Use defense in depth: Framework → Application → Parser validation
- Monitor and log validation failures for security analysis
Example - Flask Defense in Depth:
from flask import Flask, request
from valid8r import parsers
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 # Layer 1: Framework limit
@app.route('/submit', methods=['POST'])
def submit():
phone = request.form.get('phone', '')
# Layer 2: Application validation
if len(phone) > 100:
return "Phone too long", 400
# Layer 3: Parser validation
result = parsers.parse_phone(phone)
if result.is_failure():
return "Invalid phone format", 400
return process_phone(result.value_or(None))See Production Deployment Guide for framework-specific examples (Flask, Django, FastAPI).
Valid8r provides input validation, not protection against:
- ❌ SQL injection - Use parameterized queries / ORMs
- ❌ XSS attacks - Use output encoding / template engines
- ❌ CSRF attacks - Use CSRF tokens / SameSite cookies
- ❌ DDoS attacks - Use rate limiting / CDN / WAF
Parser Input Limits:
| Parser | Max Input | Notes |
|---|---|---|
parse_email() |
254 chars | RFC 5321 maximum |
parse_phone() |
100 chars | International + extension |
parse_url() |
2048 chars | Browser URL limit |
parse_uuid() |
36 chars | Standard UUID format |
parse_ip() |
45 chars | IPv6 maximum |
All parsers include built-in DoS protection with early length validation before expensive operations.
See SECURITY.md for complete security documentation.
Join the Valid8r community on GitHub Discussions:
- Questions? Start a discussion in Q&A
- Feature ideas? Share them in Ideas
- Built something cool? Show it off in Show and Tell
- Announcements: Watch Announcements for updates
When to use Discussions vs Issues:
- Use Discussions for questions, ideas, and general conversation
- Use Issues for bug reports and feature requests with technical specifications
See the Welcome Discussion for community guidelines.
We welcome contributions! All contributions must be made via forks - please do not create branches directly in the main repository.
See CONTRIBUTING.md for complete guidelines.
Quick links:
- Fork-Based Workflow Requirement
- Code of Conduct
- Development Setup
- Commit Message Format
- Pull Request Process
# 1. Fork the repository on GitHub
# Visit: https://github.com/mikelane/valid8r
# 2. Clone YOUR fork (not the upstream repo)
git clone https://github.com/YOUR-USERNAME/valid8r
cd valid8r
# 3. Add upstream remote
git remote add upstream https://github.com/mikelane/valid8r.git
# 4. Install uv (fast dependency manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 5. Install dependencies
uv sync
# 6. Run tests
uv run tox
# 7. Run linters
uv run ruff check .
uv run ruff format .
uv run mypy valid8r
# 8. Create a feature branch and make your changes
git checkout -b feat/your-feature
# 9. Push to YOUR fork and create a PR
git push origin feat/your-featureValid8r is in active development (v0.7.x). The API is stabilizing but may change before v1.0.0.
- ✅ Core parsers and validators
- ✅ Maybe monad error handling
- ✅ Interactive prompting
- ✅ Network parsers (URL, Email, IP, Phone)
- ✅ Collection parsers
- ✅ Comprehensive testing utilities
- 🚧 Additional validators (in progress)
- 🚧 Custom error types (planned)
See ROADMAP.md for planned features.
MIT License - see LICENSE for details.
Copyright (c) 2025 Mike Lane
Made with ❤️ for the Python community