Persistent/Functional/Immutable data structures for Python
—
Structured data types with fixed schemas, type checking, and serialization support. PRecord provides a dict-like interface while PClass provides an object-like interface, both with field specifications and validation.
Dict-like persistent data structure with fixed schema and field validation. Extends PMap with type checking and field constraints.
class PRecord(PMap):
"""
Persistent record with fixed fields and optional validation.
Define fields as class attributes using field() specifications.
"""
_precord_fields: dict
_precord_initial_values: dict
def __init__(self, **kwargs): ...
def set(self, *args, **kwargs) -> 'PRecord':
"""
Set one or more fields, returning new PRecord instance.
Supports both positional (key, value) and keyword arguments.
Unlike PMap.set(), accepts multiple key-value pairs.
"""
@classmethod
def create(cls, kwargs: dict, _factory_fields=None, ignore_extra: bool = False) -> 'PRecord':
"""
Create PRecord instance from dictionary with validation.
Parameters:
- kwargs: Dictionary of field values
- ignore_extra: If True, ignore fields not defined in schema
Returns:
New PRecord instance
"""
def serialize(self, format=None) -> dict:
"""
Serialize record using field serializers.
Parameters:
- format: Optional format parameter passed to field serializers
Returns:
Dictionary with serialized field values
"""
def discard(self, key) -> 'PRecord':
"""Return new PRecord without specified field."""
def remove(self, key) -> 'PRecord':
"""Return new PRecord without specified field (raises KeyError if missing)."""Object-like persistent data structure with fixed schema and field validation. Provides attribute-style access to fields.
class PClass:
"""
Persistent class with fixed fields and optional validation.
Define fields as class attributes using field() specifications.
Provides object-style attribute access.
"""
def __new__(cls, **kwargs): ...
def set(self, *args, **kwargs) -> 'PClass':
"""
Set one or more fields, returning new PClass instance.
Supports both positional (name, value) and keyword arguments.
"""
@classmethod
def create(cls, kwargs: dict, _factory_fields=None, ignore_extra: bool = False) -> 'PClass':
"""
Create PClass instance from dictionary with validation.
Parameters:
- kwargs: Dictionary of field values
- ignore_extra: If True, ignore fields not defined in schema
Returns:
New PClass instance
"""
def serialize(self, format=None) -> dict:
"""
Serialize class using field serializers.
Parameters:
- format: Optional format parameter passed to field serializers
Returns:
Dictionary with serialized field values
"""
def transform(self, *transformations) -> 'PClass':
"""Apply path-based transformations to nested structure."""
def evolver(self) -> 'PClassEvolver':
"""Return mutable-like interface for efficient batch updates."""
def remove(self, name: str) -> 'PClass':
"""Return new PClass instance without specified field."""
class PClassMeta(type):
"""Metaclass for PClass that processes field definitions."""
class PClassEvolver:
"""Mutable-like interface for efficient PClass updates."""
def __init__(self, original: PClass, initial_dict: dict): ...
def __getitem__(self, item): ...
def __setitem__(self, key, value): ...
def __delitem__(self, item): ...
def set(self, key, value) -> 'PClassEvolver': ...
def remove(self, item) -> 'PClassEvolver': ...
def persistent(self) -> PClass: ...
def __getattr__(self, item): ...Define field schemas with type checking, validation, defaults, and serialization.
def field(
type=(),
invariant=lambda _: (True, None),
initial=object(),
mandatory: bool = False,
factory=lambda x: x,
serializer=lambda _, value: value
) -> 'PField':
"""
Define a field specification for PRecord or PClass.
Parameters:
- type: Required type(s) - single type, tuple of types, or empty tuple for any
- invariant: Validation function returning (bool, error_msg) tuple
- initial: Default value (use object() for no default)
- mandatory: If True, field must be provided during creation
- factory: Function to transform input values
- serializer: Function to transform values during serialization
Returns:
PField specification object
"""Pre-configured field types for persistent collections with type checking.
def pset_field(
item_type,
optional: bool = False,
initial=(),
invariant=lambda _: (True, None),
item_invariant=lambda _: (True, None)
) -> 'PField':
"""
Create a field that holds a type-checked PSet.
Parameters:
- item_type: Required type for set elements
- optional: If True, field can be None
- initial: Default PSet contents
- invariant: Additional validation function for the field
- item_invariant: Additional validation function for individual items
Returns:
PField for CheckedPSet
"""
def pvector_field(
item_type,
optional: bool = False,
initial=(),
invariant=lambda _: (True, None),
item_invariant=lambda _: (True, None)
) -> 'PField':
"""
Create a field that holds a type-checked PVector.
Parameters:
- item_type: Required type for vector elements
- optional: If True, field can be None
- initial: Default PVector contents
- invariant: Additional validation function for the field
- item_invariant: Additional validation function for individual items
Returns:
PField for CheckedPVector
"""
def pmap_field(
key_type,
value_type,
optional: bool = False,
initial=None,
invariant=lambda _: (True, None)
) -> 'PField':
"""
Create a field that holds a type-checked PMap.
Parameters:
- key_type: Required type for map keys
- value_type: Required type for map values
- optional: If True, field can be None
- initial: Default PMap contents (defaults to empty pmap)
- invariant: Additional validation function
Returns:
PField for CheckedPMap
"""
class PField:
"""Field specification object (internal use)."""class PTypeError(TypeError):
"""
Type error for record/class fields.
Attributes:
- source_class: Class that raised the error
- field: Field name that caused the error
- expected_types: Tuple of expected types
- actual_type: Actual type that was provided
"""
source_class: type
field: str
expected_types: tuple
actual_type: typefrom pyrsistent import PRecord, field
class Person(PRecord):
name = field(type=str, mandatory=True)
age = field(type=int, initial=0)
email = field(type=str, initial='')
# Create instances
person = Person(name='Alice', age=30, email='alice@example.com')
person2 = Person(name='Bob') # Uses default age=0, email=''
# Update fields (returns new instance)
older_person = person.set(age=31)
updated_person = person.set(age=31, email='alice@newdomain.com')
# Access like a dictionary
print(person['name']) # 'Alice'
print(person.get('age', 0)) # 30
# Validation happens automatically
try:
Person(name=123) # Invalid type for name
except PTypeError as e:
print(f"Type error in field {e.field}: expected {e.expected_types}, got {e.actual_type}")from pyrsistent import PClass, field
class Point(PClass):
x = field(type=(int, float), initial=0)
y = field(type=(int, float), initial=0)
# Create instances
point = Point(x=1, y=2)
origin = Point() # Uses defaults x=0, y=0
# Update fields (returns new instance)
moved_point = point.set(x=5, y=10)
# Access like object attributes
print(point.x) # 1
print(point.y) # 2
# Attribute-style access
distance_squared = point.x**2 + point.y**2from pyrsistent import PRecord, field, pset_field, pvector_field
class Product(PRecord):
name = field(
type=str,
mandatory=True
)
price = field(
type=(int, float),
mandatory=True,
invariant=lambda price: (price > 0, "Price must be positive")
)
tags = pset_field(
item_type=str,
initial=pset()
)
reviews = pvector_field(
item_type=str,
initial=pvector()
)
metadata = field(
type=dict,
initial={},
factory=lambda d: d.copy(), # Ensure we get a copy
serializer=lambda _, value: dict(value) # Convert to regular dict for JSON
)
# Create product
product = Product(
name='Laptop',
price=999.99,
tags=pset(['electronics', 'computers']),
reviews=pvector(['Great laptop!', 'Fast delivery'])
)
# Validation works
try:
Product(name='Invalid', price=-100) # Negative price
except InvariantException as e:
print(f"Invariant failed: {e}")class User(PRecord):
username = field(type=str, mandatory=True)
created_at = field(
type=str,
factory=lambda dt: dt.isoformat() if hasattr(dt, 'isoformat') else str(dt),
serializer=lambda _, value: value # Already converted by factory
)
preferences = field(
type=dict,
initial={},
serializer=lambda _, prefs: {k: v for k, v in prefs.items() if v is not None}
)
from datetime import datetime
user = User(
username='alice',
created_at=datetime.now(),
preferences={'theme': 'dark', 'notifications': True, 'temp': None}
)
# Serialize for JSON/API output
user_data = user.serialize()
# {'username': 'alice', 'created_at': '2023-...', 'preferences': {'theme': 'dark', 'notifications': True}}# Use create() for construction from external data
external_data = {
'name': 'Alice',
'age': '30', # String that needs conversion
'unknown_field': 'ignored'
}
class StrictPerson(PRecord):
name = field(type=str, mandatory=True)
age = field(type=int, factory=int) # Convert strings to int
# Ignore extra fields
person = StrictPerson.create(external_data, ignore_extra=True)
# Or handle unknown fields
try:
StrictPerson.create(external_data)
except Exception as e:
print(f"Unknown field error: {e}")class Config(PClass):
debug = field(type=bool, initial=False)
port = field(type=int, initial=8080)
host = field(type=str, initial='localhost')
config = Config()
# Efficient batch updates
evolver = config.evolver()
evolver.debug = True
evolver.port = 3000
evolver.host = '0.0.0.0'
new_config = evolver.persistent()
# Or using set method
evolver2 = config.evolver()
evolver2.set('debug', True).set('port', 3000)
new_config2 = evolver2.persistent()Install with Tessl CLI
npx tessl i tessl/pypi-pyrsistent