Skip to main content

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 names
  • ValidationError - Pydantic validation failures
  • TypeError - 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:

  1. Pydantic v2 - Has model_copy method → uses model_validate
  2. Dataclass - Detected via dataclasses.is_dataclass() → uses dataclasses.replace
  3. Fallback - Attempts copy.replace() (Python 3.13+)
  4. Unsupported - Returns TypeError in Resolute.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 with keyword
  • Uses the resolute package for Result types
  • Built with ❤️ for type-safe Python

See Also

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

validate_with_resolute-0.9.0.tar.gz (18.2 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

validate_with_resolute-0.9.0-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

validate_with_resolute-0.9.0-1-py3-none-any.whl (17.9 kB view details)

Uploaded Python 3

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

Hashes for validate_with_resolute-0.9.0.tar.gz
Algorithm Hash digest
SHA256 cea09c5ae4beffee3975593398206f9fc46656834f181ff90907365983992aba
MD5 b6a5d734a1e1224f2e1dcb06f5ba2334
BLAKE2b-256 ce76e13a33975cf0216c11ee39af7f6745d14f582d3bd3e885dc861d44671a90

See more details on using hashes here.

File details

Details for the file validate_with_resolute-0.9.0-py3-none-any.whl.

File metadata

File hashes

Hashes for validate_with_resolute-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f6a89bc728bff968e9268ff99fb9b346d0444a582b9b2d373d299a0faa6fe2d8
MD5 8591f2121684d80beb30c269b551e305
BLAKE2b-256 c148fd0511b1292d6d2a36c11737d7c685fa3d7b2fd14f9553aca832446697aa

See more details on using hashes here.

File details

Details for the file validate_with_resolute-0.9.0-1-py3-none-any.whl.

File metadata

File hashes

Hashes for validate_with_resolute-0.9.0-1-py3-none-any.whl
Algorithm Hash digest
SHA256 02b75a1dffee80fd4289dc027213d7e679b6a78f51e58cdc7bfec0251ef8d061
MD5 af37a4ac0ae9266f74d08716eaec401b
BLAKE2b-256 c0b37f624fdbbefbf0c5a5f488a1426e32736ea0f9be3002e60a62d21555c300

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page