Parallel validation and serialization for Python 3.13+
Project description
paravalid
Parallel validation and serialization for Python 3.13+
paravalid provides high-performance parallel JSON serialization/deserialization and Pydantic model validation optimized for Python 3.13+ free-threaded (no-GIL) builds.
Features
- Parallel JSON operations: Serialize and deserialize JSON lists in parallel
- Parallel Pydantic validation: Validate data against Pydantic models using multiple threads
- Automatic fallback: Seamlessly falls back to sequential processing when no-GIL is unavailable
- Preserves order: Results always maintain the original input order
- Error handling: Choose between raising on first error or collecting all errors
- Smart cancellation: When
errors='raise', remaining tasks are cancelled immediately on first error - Zero overhead: Small datasets automatically use sequential processing
- CLI tool: Check no-GIL availability and configuration
Performance
Tested on Python 3.14.0rc2 (free-threaded), GIL disabled, 8 workers:
| Operation | Dataset Size | Speedup |
|---|---|---|
| JSON serialization | 5,000 objects | 4.66× |
| Pydantic validation | 5,000 users | 3.36× |
Detailed Benchmark Results
Complex Pydantic Models (2-4KB JSON)
Testing with nested models containing 7+ fields, nested objects, and lists:
Python 3.13.7t (free-threaded):
| Items | Sequential | Parallel (chunk=64) | Speedup |
|---|---|---|---|
| 500 | 9.10 ms | 4.06 ms | 2.24× |
| 1000 | 14.82 ms | 7.17 ms | 2.07× |
Python 3.14.0t (free-threaded):
| Items | Sequential | Parallel (chunk=64) | Speedup |
|---|---|---|---|
| 500 | 7.45 ms | 3.44 ms | 2.16× |
| 1000 | 13.71 ms | 6.92 ms | 1.98× |
Key Findings:
- ✅ Optimal chunk size: 64 for complex models
- ✅ Consistent 1.7-2.2× speedup across different chunk sizes (64, 128, 256, auto)
- ✅ Python 3.14 shows 10-18% faster sequential baseline
- ✅ Best results with models containing nested structures and validation logic
Large JSON Objects (50KB+ per object)
Testing with large documents containing extensive text, lists, and metadata:
Python 3.13.7t (free-threaded):
| Items | Operation | Sequential | Parallel (8 workers) | Speedup |
|---|---|---|---|---|
| 5,000 | Dumps | 573.01 ms | 177.36 ms | 3.23× |
| 5,000 | Loads | 819.09 ms | 200.31 ms | 4.09× |
| 10,000 | Dumps | 1155.57 ms | 345.14 ms | 3.35× |
| 10,000 | Loads | 1391.30 ms | 440.00 ms | 3.16× |
Python 3.14.0t (free-threaded):
| Items | Operation | Sequential | Parallel (8 workers) | Speedup |
|---|---|---|---|---|
| 5,000 | Dumps | 603.98 ms | 182.85 ms | 3.30× |
| 5,000 | Loads | 486.63 ms | 254.20 ms | 1.91× |
| 10,000 | Dumps | 1185.10 ms | 310.86 ms | 3.81× |
| 10,000 | Loads | 917.45 ms | 372.49 ms | 2.46× |
Key Findings:
- ✅ 3-4× speedup for large objects (50KB+ per object)
- ✅ JSON dumps consistently shows 3.2-3.8× speedup
- ✅ JSON loads shows 1.9-4.1× speedup depending on Python version
- ✅ Performance improves with dataset size (10k items better than 5k)
- ⚠️ Small simple objects (<1KB) don't benefit due to thread overhead
Performance Guidelines
When to use paravalid:
- ✅ Complex Pydantic models with nested structures (2KB+ JSON)
- ✅ Large JSON objects (10KB+ per object)
- ✅ Dataset size ≥ 500 items for complex models
- ✅ Dataset size ≥ 5,000 items for large JSON objects
- ✅ CPU-bound validation logic in your models
When NOT to use:
- ❌ Simple flat objects (<500 bytes)
- ❌ Small datasets (<100 items)
- ❌ Operations dominated by I/O rather than CPU
Optimal Settings:
- Workers: 8 threads on modern CPUs (4-8 cores)
- chunk_size: 64 for Pydantic validation
- min_size: 100 (default) for complex models, 500-1000 for JSON
Installation
# Basic installation (JSON support only)
pip install paravalid
# With Pydantic support
pip install paravalid[pydantic]
Requirements
- Python 3.13 or higher
- Optional: Pydantic 2.0+ for validation features
Quick Start
JSON Serialization
from paravalid import parallel_json_dumps, parallel_json_loads
# Serialize a list of objects
data = [{"id": i, "name": f"user_{i}"} for i in range(1000)]
json_strings = parallel_json_dumps(data, workers=4)
# Deserialize back
objects = parallel_json_loads(json_strings, workers=4)
Pydantic Validation
from pydantic import BaseModel
from paravalid import parallel_validate
class User(BaseModel):
id: int
name: str
email: str
age: int
# Validate data in parallel
data = [
{"id": i, "name": f"user_{i}", "email": f"user{i}@example.com", "age": 25}
for i in range(1000)
]
users = parallel_validate(User, data, workers=4)
API Reference
JSON Operations
parallel_json_dumps
parallel_json_dumps(
obj_list: list[Any],
*,
workers: Literal["auto"] | int = "auto",
min_size: int = 100,
errors: Literal["raise", "collect"] = "raise",
timeout: float | None = None,
**json_kwargs: Any,
) -> list[str | Exception]
Serialize a list of objects to JSON strings in parallel.
Parameters:
obj_list: List of objects to serializeworkers: Number of workers ('auto' uses CPU count)min_size: Minimum list size to trigger parallelization (default: 100)errors: 'raise' to raise on first error, 'collect' to return exceptions in resultstimeout: Global timeout in seconds for all operations**json_kwargs: Additional arguments passed tojson.dumps(indent, ensure_ascii, default, etc.)
Returns: List of JSON strings or exceptions (in original order)
parallel_json_loads
parallel_json_loads(
json_list: list[str],
*,
workers: Literal["auto"] | int = "auto",
min_size: int = 100,
errors: Literal["raise", "collect"] = "raise",
timeout: float | None = None,
**json_kwargs: Any,
) -> list[Any | Exception]
Deserialize a list of JSON strings to Python objects in parallel.
Parameters: Similar to parallel_json_dumps
Returns: List of deserialized objects or exceptions (in original order)
Pydantic Validation
parallel_validate
parallel_validate(
model_or_type: type[T],
data_list: list[Any],
*,
workers: Literal["auto"] | int = "auto",
min_size: int = 100,
chunk_size: Literal["auto"] | int | None = "auto",
errors: Literal["raise", "collect"] = "raise",
timeout: float | None = None,
**opts: Any,
) -> list[T | Exception]
Validate a list of data against a Pydantic model or type in parallel.
Parameters:
model_or_type: A Pydantic BaseModel subclass or any type for TypeAdapterdata_list: List of data to validateworkers: Number of workers ('auto' uses CPU count)min_size: Minimum list size to trigger parallelization (default: 100)chunk_size: Size of chunks for parallel processing'auto': Conservative default (len(data) // (workers * 6), min 1)None: No chunking (one item per task)int: Explicit chunk size
errors: 'raise' to raise on first error, 'collect' to return exceptions in resultstimeout: Global timeout in seconds for all operations
Returns: List of validated objects or exceptions (in original order)
Utility Functions
is_nogil_available
is_nogil_available() -> bool
Check if the Python build supports running without the GIL.
is_nogil_active
is_nogil_active() -> bool
Check if the GIL is currently disabled at runtime.
Advanced Usage
Error Collection
Instead of raising on the first error, collect all errors and continue processing:
from paravalid import parallel_json_loads
json_strings = ['{"valid": 1}', 'invalid json', '{"also": "valid"}']
results = parallel_json_loads(json_strings, errors="collect")
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"Item {i} failed: {result}")
else:
print(f"Item {i} succeeded: {result}")
Custom JSON Serialization
Pass additional arguments to json.dumps:
from paravalid import parallel_json_dumps
data = [{"name": "François"}, {"name": "José"}]
# Pretty-print with indentation
json_strings = parallel_json_dumps(data, indent=2)
# Preserve Unicode characters
json_strings = parallel_json_dumps(data, ensure_ascii=False)
# Custom default handler
def custom_default(obj):
if isinstance(obj, set):
return list(obj)
raise TypeError(f"Cannot serialize {type(obj)}")
json_strings = parallel_json_dumps(data, default=custom_default)
Nested Pydantic Models
from pydantic import BaseModel
from paravalid import parallel_validate
class Address(BaseModel):
street: str
city: str
zipcode: str
class User(BaseModel):
name: str
email: str
address: Address
data = [
{
"name": "Alice",
"email": "alice@example.com",
"address": {"street": "123 Main St", "city": "Springfield", "zipcode": "12345"}
},
# ... more records
]
users = parallel_validate(User, data, workers=4)
Using TypeAdapter
For validating simple types or generic types:
from paravalid import parallel_validate
# Validate integers
int_strings = ["1", "2", "3", "4", "5"]
integers = parallel_validate(int, int_strings)
# Validate lists
list_data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
validated_lists = parallel_validate(list[int], list_data)
CLI Tool
paravalid includes a command-line tool to check your Python environment:
# Check if no-GIL is available and active
$ paravalid --check-nogil
No-GIL available: True
No-GIL active: True
✓ paravalid can use parallel execution with no-GIL
# Show version
$ paravalid --version
paravalid 0.1.0
# Show help
$ paravalid --help
The CLI tool helps you verify that:
- Your Python build supports free-threading (no-GIL)
- The GIL is actually disabled at runtime
paravalidwill be able to use parallel execution
Exit codes:
0: No-GIL is available and active (parallel execution enabled)1: No-GIL is not available or not active (will use sequential fallback)
How It Works
- No-GIL Detection: Automatically detects if Python is running in free-threaded mode using:
sysconfig.get_config_var("Py_GIL_DISABLED")(most reliable)sys.versionstring check for "free-threading"sys._is_gil_enabled()runtime API
- Smart Parallelization: Only uses parallel processing when:
- No-GIL is active
- Dataset size ≥
min_size(default: 100)
- ThreadPoolExecutor: Uses threads (not processes) for true parallelism without GIL overhead
- Order Preservation: Results are always returned in the same order as input
- Task Cancellation: When
errors='raise', all remaining tasks are cancelled immediately on first error - Graceful Fallback: Falls back to sequential processing on standard CPython builds
Important Caveats
Thread Safety
- Custom
json.defaultfunctions: If you provide a customdefaultfunction toparallel_json_dumps, ensure it is thread-safe - Global state: Avoid accessing mutable global state from custom validators or default handlers
- Pydantic validators: Any custom validators in your Pydantic models must be thread-safe
Performance Considerations
- Small datasets: Parallelization adds overhead; adjust
min_sizebased on your data - CPU-bound only: This library targets CPU-bound operations, not I/O-bound tasks
- Memory: Parallel processing uses more memory as multiple operations run simultaneously
Running Tests
# Install development dependencies
pip install -e ".[pydantic]"
pip install pytest
# Run all tests
pytest tests/ -v
# Run specific test modules
pytest tests/test_json.py -v
pytest tests/test_pydantic.py -v
Running Benchmarks
# Install benchmark dependencies
pip install pyperf
# Run JSON benchmarks
python benchmarks/bench_json.py
# Run Pydantic benchmarks
python benchmarks/bench_pydantic.py
Examples
See the examples/ directory for complete working examples:
examples/example_json.py: JSON serialization examplesexamples/example_pydantic.py: Pydantic validation examples
License
This project is provided as-is under the MIT License.
Contributing
Contributions are welcome! Please ensure:
- All tests pass
- Code follows the existing style
- New features include tests and documentation
Changelog
0.1.0 (2025-10-11)
- Initial release
- Parallel JSON serialization/deserialization
- Parallel Pydantic validation
- Automatic no-GIL detection with
sysconfig.get_config_var("Py_GIL_DISABLED")priority - Error collection support with
errors='collect' - Smart task cancellation: when
errors='raise', remaining tasks are cancelled immediately - Proper
concurrent.futures.TimeoutErrorhandling - CLI tool with
--check-nogilcommand - Comprehensive test suite (52 tests)
- Detailed benchmarks showing 2-4× speedup on complex models
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paravalid-0.1.0.tar.gz.
File metadata
- Download URL: paravalid-0.1.0.tar.gz
- Upload date:
- Size: 8.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
527d7ca8319cd463f1bda9763facd22e8066f8cf6e88ed4355e2c270a875b4bd
|
|
| MD5 |
ebfb140b322ba734dd7902939b9108ec
|
|
| BLAKE2b-256 |
288f87f4afe2a4dbf19eeed6cf8e3f108320f9dac39d42d77edbe977d2113f7b
|
File details
Details for the file paravalid-0.1.0-py3-none-any.whl.
File metadata
- Download URL: paravalid-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
661df0e6cf26a9fecb545d02da7948563db7f59bec5376c570a0b5e62c840471
|
|
| MD5 |
5a32f7af90174ed126e211ffc7bbe065
|
|
| BLAKE2b-256 |
a436a30abd03e4613e6b9791214edb2cf6f476d64a1a9fe21ce3e2022bb34a7c
|