Persistent/Functional/Immutable data structures for Python
—
Helper functions for converting between mutable and immutable structures, applying transformations, and accessing nested data. These utilities bridge the gap between regular Python data structures and persistent collections.
Convert between mutable Python built-in types and persistent collections, enabling easy integration with existing codebases.
def freeze(obj):
"""
Recursively convert mutable Python structures to persistent equivalents.
Conversions:
- dict -> pmap
- list -> pvector
- set -> pset
- tuple -> tuple (preserved)
- Other types -> unchanged
Parameters:
- obj: Object to convert
Returns:
Persistent version of the input structure
"""
def thaw(obj):
"""
Recursively convert persistent structures to mutable Python equivalents.
Conversions:
- pmap -> dict
- pvector -> list
- pset -> set
- tuple -> tuple (preserved)
- Other types -> unchanged
Parameters:
- obj: Object to convert
Returns:
Mutable version of the input structure
"""
def mutant(fn) -> callable:
"""
Decorator that automatically freezes function arguments and return value.
Useful for integrating persistent data structures with functions that
expect mutable inputs or for ensuring immutability of function results.
Parameters:
- fn: Function to decorate
Returns:
Decorated function that freezes args and return value
"""Safely access and manipulate nested data structures using key paths.
def get_in(keys: Iterable, coll: Mapping, default=None, no_default: bool = False):
"""
Get value from nested mapping structure using sequence of keys.
Equivalent to coll[keys[0]][keys[1]]...[keys[n]] with safe fallback.
Parameters:
- keys: Sequence of keys for nested access
- coll: Nested mapping structure to access
- default: Value to return if path doesn't exist
- no_default: If True, raise KeyError instead of returning default
Returns:
Value at the nested path, or default if path doesn't exist
Raises:
KeyError: If path doesn't exist and no_default=True
"""Functions for use with the .transform() method of persistent collections to apply path-based modifications.
def inc(x: int) -> int:
"""
Increment numeric value by 1.
Transformation function for use with .transform().
Parameters:
- x: Numeric value to increment
Returns:
x + 1
"""
def discard(evolver, key) -> None:
"""
Remove element from evolver during transformation.
Transformation function that removes a key/element from the
collection being transformed.
Parameters:
- evolver: Collection evolver (PMapEvolver, PVectorEvolver, PSetEvolver)
- key: Key/index/element to remove
"""
def rex(expr: str) -> callable:
"""
Create regex matcher for transformation paths.
Returns a function that tests if a string matches the regex pattern.
Useful for selecting which keys/paths to transform.
Parameters:
- expr: Regular expression pattern
Returns:
Function that tests strings against the regex
"""
def ny(_) -> bool:
"""
Matcher that always returns True.
Useful as a catch-all matcher in transformations when you want
to match any value.
Parameters:
- _: Any value (ignored)
Returns:
True (always)
"""Create namedtuple-like immutable classes with optional field validation.
def immutable(
members: Union[str, Iterable[str]] = '',
name: str = 'Immutable',
verbose: bool = False
) -> type:
"""
Create an immutable namedtuple-like class.
Creates a class similar to collections.namedtuple but with additional
immutability guarantees and optional field validation.
Parameters:
- members: Field names as string (space/comma separated) or iterable
- name: Name for the created class
- verbose: If True, print the generated class definition
Returns:
Immutable class type with specified fields
"""from pyrsistent import freeze, thaw, pmap, pvector
# Convert nested mutable structures to persistent
mutable_data = {
'users': [
{'name': 'Alice', 'tags': {'admin', 'active'}},
{'name': 'Bob', 'tags': {'user', 'active'}}
],
'config': {
'debug': True,
'features': ['auth', 'logging']
}
}
# Recursively convert to persistent structures
persistent_data = freeze(mutable_data)
# Result: pmap({
# 'users': pvector([
# pmap({'name': 'Alice', 'tags': pset(['admin', 'active'])}),
# pmap({'name': 'Bob', 'tags': pset(['user', 'active'])})
# ]),
# 'config': pmap({
# 'debug': True,
# 'features': pvector(['auth', 'logging'])
# })
# })
# Convert back to mutable for JSON serialization or external APIs
mutable_again = thaw(persistent_data)
import json
json_str = json.dumps(mutable_again)from pyrsistent import mutant, pmap
@mutant
def process_user_data(data):
"""
Function that expects and returns mutable data, but we want to
use persistent structures internally for safety.
"""
# data is automatically frozen (converted to persistent)
# Work with persistent data safely
if 'email' not in data:
data = data.set('email', '')
data = data.set('processed', True)
# Return value is automatically frozen
return data
# Use with mutable input - automatically converted
user_dict = {'name': 'Alice', 'age': 30}
result = process_user_data(user_dict)
# result is a pmap, input dict is unchangedfrom pyrsistent import get_in, pmap, pvector
# Complex nested structure
data = pmap({
'api': pmap({
'v1': pmap({
'endpoints': pvector([
pmap({'path': '/users', 'methods': pvector(['GET', 'POST'])}),
pmap({'path': '/posts', 'methods': pvector(['GET', 'POST', 'DELETE'])})
])
})
}),
'config': pmap({
'database': pmap({
'host': 'localhost',
'port': 5432
})
})
})
# Safe nested access
db_host = get_in(['config', 'database', 'host'], data) # 'localhost'
api_endpoints = get_in(['api', 'v1', 'endpoints'], data) # pvector([...])
missing = get_in(['config', 'cache', 'ttl'], data, default=300) # 300
# Access with index for vectors
first_endpoint = get_in(['api', 'v1', 'endpoints', 0, 'path'], data) # '/users'
# Raise error if path doesn't exist
try:
get_in(['nonexistent', 'path'], data, no_default=True)
except KeyError:
print("Path not found")from pyrsistent import pmap, pvector, inc, discard, rex, ny
# Apply transformations to nested structures
data = pmap({
'counters': pmap({'page_views': 100, 'api_calls': 50}),
'users': pvector(['alice', 'bob', 'charlie']),
'temp_data': 'to_be_removed'
})
# Increment all counters
transformed = data.transform(
['counters', ny], inc # For any key in counters, apply inc function
)
# Result: counters become {'page_views': 101, 'api_calls': 51}
# Remove elements matching pattern
transformed2 = data.transform(
[rex(r'temp_.*')], discard # Remove any key matching temp_*
)
# Result: 'temp_data' key is removed
# Complex transformation combining multiple operations
def process_user(user):
return user.upper() if isinstance(user, str) else user
transformed3 = data.transform(
['users', ny], process_user, # Transform all users
['counters', 'page_views'], lambda x: x * 2, # Double page views
['temp_data'], discard # Remove temp data
)from pyrsistent import immutable
# Create immutable point class
Point = immutable('x y', name='Point')
p1 = Point(x=1, y=2)
p2 = Point(3, 4) # Positional args also work
print(p1.x, p1.y) # 1 2
print(p1) # Point(x=1, y=2)
# Immutable - cannot modify
try:
p1.x = 5 # Raises AttributeError
except AttributeError:
print("Cannot modify immutable object")
# Create new instances with _replace
p3 = p1._replace(x=10) # Point(x=10, y=2)
# With more complex fields
Person = immutable('name age email', name='Person')
person = Person('Alice', 30, 'alice@example.com')
# Support for tuple unpacking
name, age, email = personfrom pyrsistent import freeze, thaw, pmap
import json
# Working with JSON APIs
def load_config(filename):
"""Load configuration from JSON file into persistent structure."""
with open(filename) as f:
mutable_config = json.load(f)
return freeze(mutable_config)
def save_config(config, filename):
"""Save persistent configuration to JSON file."""
with open(filename, 'w') as f:
json.dump(thaw(config), f, indent=2)
# Thread-safe configuration management
class ConfigManager:
def __init__(self, initial_config):
self._config = freeze(initial_config)
def get_config(self):
return self._config # Safe to share between threads
def update_config(self, updates):
# Atomic update - no race conditions
self._config = self._config.update(freeze(updates))
def get_setting(self, *path):
return get_in(path, self._config)
# Usage
config_mgr = ConfigManager({'database': {'host': 'localhost', 'port': 5432}})
db_host = config_mgr.get_setting('database', 'host')
config_mgr.update_config({'database': {'timeout': 30}})Install with Tessl CLI
npx tessl i tessl/pypi-pyrsistent