Immutable object modifications with the Result pattern - inspired by C# records
Project description
validate-with-resolute
Immutable object modifications with the Result pattern - inspired by C# records.
validate-with-resolute provides a safe, type-preserving way to modify dataclasses and Pydantic models using the Result pattern. All validation errors are captured and returned—no exceptions escape the API.
Features
- ✅ No exceptions escape - All errors captured in
Resolute[T] - ✅ Type-safe - Full type preservation with generics
- ✅ Immutable - Original objects never modified
- ✅ Framework support - Works with dataclasses and Pydantic v2
- ✅ Error accumulation - Multiple validation errors collected together
- ✅ Flexible API - Use standalone function or decorator
Installation
pip install validate-with-resolute
Requirements: Python 3.13+
Quick Start
✅ Recommended: Record base class
Best option for type checking and autocomplete:
from dataclasses import dataclass
from validate_with_resolute import Record
@dataclass
class User(Record):
name: str
age: int
# Create with validation error handling
result = User.from_(name="Bob", age=25)
if result.is_success:
user = result.value
# Modify with .with_() method - full autocomplete support!
result = user.with_(name="Alice", age=30)
if result.is_success:
print(result.value.name) # "Alice"
print(user.name) # "Bob" - original unchanged
Works with Pydantic too:
from pydantic import BaseModel, field_validator
from validate_with_resolute import Record
class User(BaseModel, Record):
name: str
age: int
@field_validator('age')
@classmethod
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('age must be positive')
return v
# Safe creation - validation errors captured!
result = User.from_(name="Bob", age=-5)
if result.has_errors:
print(result.errors) # [ValidationError(...)]
# Safe modification
user = User(name="Bob", age=25)
result = user.with_(age=30) # Autocomplete works!
Alternative: Standalone modify() function
If you prefer not to use inheritance:
from dataclasses import dataclass
from validate_with_resolute import modify
@dataclass
class User:
name: str
age: int
user = User(name="Bob", age=25)
# Successful modification
result = modify(user, name="Alice")
if result.is_success:
print(result.value.name) # "Alice"
Usage Examples
Multiple changes in one call
result = modify(user, name="Charlie", age=35, active=True)
Pydantic validation
from pydantic import BaseModel, field_validator
from validate_with_resolute import with_modify
@with_modify
class User(BaseModel):
name: str
age: int
@field_validator('age')
@classmethod
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('age must be non-negative')
return v
user = User(name="Bob", age=25)
result = user.with_(age=-5)
# Validation error captured, not raised
assert result.has_errors
print(result.errors) # [ValidationError(...)]
Error accumulation
# Multiple errors collected together
result = modify(user, invalid_field="x", another_bad="y")
assert len(result.errors) == 2
Safe instance creation with from_()
from pydantic import BaseModel, field_validator
from validate_with_resolute import Record
class User(BaseModel, Record):
name: str
age: int
email: str
@field_validator('age')
@classmethod
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('age must be positive')
return v
@field_validator('email')
@classmethod
def validate_email(cls, v: str) -> str:
if '@' not in v:
raise ValueError('invalid email')
return v
# Traditional approach - raises exception
try:
user = User(name="Bob", age=-5, email="invalid")
except Exception as e:
print(f"Exception: {e}")
# With Record.from_() - returns Result
result = User.from_(name="Bob", age=-5, email="invalid")
if result.has_errors:
for error in result.errors:
print(f"Validation error: {error}")
# All validation errors captured, no exception!
Nested objects
@dataclass
class Address:
city: str
country: str
@dataclass
class Person:
name: str
address: Address
address = Address(city="NYC", country="USA")
person = Person(name="Bob", address=address)
# Modify nested object first
new_address_result = modify(address, city="LA")
# Then update parent
result = modify(person, address=new_address_result.value)
API Reference
Record (Base Class)
✅ Recommended: Base class that provides .from_() and .with_() methods with full type checking support.
Class Methods:
Record.from_(**kwargs) -> Resolute[T]
Create a new instance with validation errors wrapped in Resolute (no exceptions raised).
from dataclasses import dataclass
from validate_with_resolute import Record
@dataclass
class User(Record):
name: str
age: int
# Safe creation - validation errors captured
result = User.from_(name="Bob", age=25)
if result.is_success:
user = result.value
else:
print(result.errors) # All errors captured
Perfect for:
- Processing user input
- Parsing external data
- API request handling
- Any scenario where construction might fail
instance.with_(**changes) -> Resolute[Self]
Modify an existing instance with validation errors wrapped in Resolute.
user = User(name="Bob", age=25)
result = user.with_(name="Alice") # Full autocomplete & type checking!
if result.is_success:
updated = result.value
Works with Pydantic:
from pydantic import BaseModel, field_validator
from validate_with_resolute import Record
class User(BaseModel, Record):
name: str
age: int
@field_validator('age')
@classmethod
def validate_age(cls, v: int) -> int:
if v < 0:
raise ValueError('age must be positive')
return v
# Both from_() and with_() capture validation errors
result = User.from_(name="Bob", age=-5) # ✓ Errors captured
result = user.with_(age=-5) # ✓ Errors captured
Benefits:
- ✅ Full autocomplete in IDEs
- ✅ Full type checking with mypy
- ✅ Works with both dataclasses and Pydantic
- ✅ No decorators needed
- ✅ Clear inheritance model
- ✅ Safe creation with
from_() - ✅ Safe modification with
with_()
modify(obj: T, **changes: Any) -> Resolute[T]
Standalone function for immutable modifications (no inheritance required).
Parameters:
obj- The object to modify (dataclass or Pydantic model)**changes- Field names and their new values
Returns:
Resolute[T]- Success with modified object, or failure with errors
Supported types:
- Dataclasses (via
dataclasses.replace) - Pydantic v2 models (via
model_validate) - Objects with
copy.replace()support (Python 3.13+)
Example:
from validate_with_resolute import modify
result = modify(user, name="Alice")
@with_modify (Decorator)
Decorator that adds .with_() method to a class (legacy approach).
Note: Prefer using Record base class instead for better type checking.
Example:
@with_modify
@dataclass
class MyClass:
field: str
result = obj.with_(field="value") # type: ignore[attr-defined]
Result Pattern
validate-with-resolute uses the resolute package for Result types.
Key methods:
result = modify(obj, field="value")
result.is_success # True if successful
result.has_errors # True if failed
result.value # The modified object (on success)
result.errors # List of errors (on failure)
Error Handling
All exceptions during modification are caught and returned in Resolute.errors:
ValueError- Invalid field namesValidationError- Pydantic validation failuresTypeError- Unsupported object types- Any other exception raised during modification
No exceptions ever escape the API.
Framework Detection
validate-with-resolute automatically detects the object type:
- Pydantic v2 - Has
model_copymethod → usesmodel_validate - Dataclass - Detected via
dataclasses.is_dataclass()→ usesdataclasses.replace - Fallback - Attempts
copy.replace()(Python 3.13+) - Unsupported - Returns
TypeErrorinResolute.errors
Why only dataclasses and Pydantic?
- These frameworks provide validated, immutable updates
- Regular Python classes would bypass
__init__validation - This ensures modifications are always-valid (core principle of validate-with-resolute)
Type Safety
Full type preservation with generics:
user: User = User(name="Bob", age=25)
result: Resolute[User] = modify(user, age=26)
# Type checkers know result.value is User
if result.is_success:
updated_user: User = result.value
Type Checking with mypy
The package includes a py.typed marker and full type annotations:
# Type check your code
mypy your_code.py --strict
Type checking comparison:
| Approach | Type Checking | Autocomplete | Recommendation |
|---|---|---|---|
Record base class |
✅ Full | ✅ Full | ⭐ Best |
modify() function |
✅ Full | ✅ Full | ✅ Great |
@with_modify decorator |
⚠️ Needs ignore | ❌ No | ⚠️ Legacy |
# ⭐ Best: Record base class
@dataclass
class User(Record):
name: str
result = user.with_(name="Alice") # ✅ Full support!
# ✅ Great: modify() function
result = modify(user, name="Alice") # ✅ Full support!
# ⚠️ Legacy: @with_modify decorator
result = user.with_(name="Alice") # type: ignore[attr-defined]
Why validate-with-resolute?
Problem:
# Traditional approach - exceptions escape
user = User(name="Bob", age=25)
try:
user.age = -5 # Might raise, might silently fail
# Or need to create new instance manually
except ValidationError as e:
# Handle error
Solution with validate-with-resolute:
# All errors captured, no exceptions
result = user.with_(age=-5)
if result.has_errors:
# All validation errors available
print(result.errors)
Design Principles
- Immutability - Original objects never modified
- Type safety - Full generic type preservation
- Error accumulation - Collect all errors, not just first
- Framework agnostic - Works with dataclasses, Pydantic, and more
- No magic - Simple, predictable behavior
Development
# Clone repository
git clone https://github.com/Marcurion/validate-with-resolute
cd validate-with-resolute
# Install in development mode
pip install -e ".[dev,pydantic]"
# Run tests
pytest tests/ -v
# Type checking
mypy src/
License
MIT License - see LICENSE file for details.
Contributing
Contributions welcome! Please open an issue or PR.
Credits
- Inspired by C# record types and the
withkeyword - Uses the
resolutepackage for Result types - Built with ❤️ for type-safe Python
See Also
- resolute - Result pattern for Python
- pydantic - Data validation library
- dataclasses - Python standard library
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 Distributions
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 validate_with_resolute-0.9.0.tar.gz.
File metadata
- Download URL: validate_with_resolute-0.9.0.tar.gz
- Upload date:
- Size: 18.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cea09c5ae4beffee3975593398206f9fc46656834f181ff90907365983992aba
|
|
| MD5 |
b6a5d734a1e1224f2e1dcb06f5ba2334
|
|
| BLAKE2b-256 |
ce76e13a33975cf0216c11ee39af7f6745d14f582d3bd3e885dc861d44671a90
|
File details
Details for the file validate_with_resolute-0.9.0-py3-none-any.whl.
File metadata
- Download URL: validate_with_resolute-0.9.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6a89bc728bff968e9268ff99fb9b346d0444a582b9b2d373d299a0faa6fe2d8
|
|
| MD5 |
8591f2121684d80beb30c269b551e305
|
|
| BLAKE2b-256 |
c148fd0511b1292d6d2a36c11737d7c685fa3d7b2fd14f9553aca832446697aa
|
File details
Details for the file validate_with_resolute-0.9.0-1-py3-none-any.whl.
File metadata
- Download URL: validate_with_resolute-0.9.0-1-py3-none-any.whl
- Upload date:
- Size: 17.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
02b75a1dffee80fd4289dc027213d7e679b6a78f51e58cdc7bfec0251ef8d061
|
|
| MD5 |
af37a4ac0ae9266f74d08716eaec401b
|
|
| BLAKE2b-256 |
c0b37f624fdbbefbf0c5a5f488a1426e32736ea0f9be3002e60a62d21555c300
|