ProvenPin Install

pyrsistent PRecord, PVector and PMap immutable data: working versions (tested 2026-10-16)

Immutable records, vectors and maps with PRecord, PVector and PMap: field and global invariants, PMap evolvers for batch updates, structural sharing and freeze/thaw snapshots, tested offline.

passed6 passed · 0 failed2026-10-16 21:57 UTC
Status
passed
Last passed
2026-10-16 21:57 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions2
pyrsistent@0.20.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38972656245

The code

Exactly the files that passed. Your agent can pull them with get_recipe("pyrsistent-immutable-records") (install), or see them on GitHub.

requirements.txt
pyrsistent==0.20.0
pytest==9.1.1
records.py
from pyrsistent import PMap, PRecord, PVector, field, freeze, pmap, pvector


class LineItem(PRecord):
    sku = field(str)
    qty = field(int, invariant=lambda q: (q > 0, "qty must be positive"))
    price_cents = field(int, invariant=lambda p: (p >= 0, "price must be >= 0"))

    @property
    def total_cents(self) -> int:
        return self.qty * self.price_cents


class Order(PRecord):
    order_id = field(str)
    lines = field(PVector, initial=pvector())
    meta = field(PMap, initial=pmap())

    __invariant__ = lambda self: (
        all(isinstance(line, LineItem) for line in self.lines),
        "lines must contain LineItem records",
    )

    @property
    def total_cents(self) -> int:
        return sum(line.total_cents for line in self.lines)


def add_line(order: Order, item: LineItem) -> Order:
    return order.set(lines=order.lines.append(item))


def retag(order: Order, **meta: str) -> Order:
    evolver = order.meta.evolver()
    for key, value in meta.items():
        evolver[key] = value
    return order.set(meta=evolver.persistent())


def snapshot(config: dict) -> PMap:
    return freeze(config)
tests/test_records.py
import pytest
from pyrsistent import InvariantException, PMap, PTypeError, PVector, freeze, pvector, thaw

from records import LineItem, Order, add_line, retag, snapshot

ITEM = LineItem(sku="tea", qty=2, price_cents=350)


def test_records_are_immutable_and_hashable():
    original = Order(order_id="o1")
    updated = add_line(original, ITEM)

    assert updated.lines == pvector([ITEM]) and original.lines == pvector()
    with pytest.raises(AttributeError):
        original.order_id = "o2"
    assert len({ITEM, LineItem(sku="tea", qty=2, price_cents=350)}) == 1


def test_field_invariant_and_type_check_are_separate():
    with pytest.raises(InvariantException) as exc:
        LineItem(sku="tea", qty=0, price_cents=350)
    assert exc.value.invariant_errors == ("qty must be positive",)

    with pytest.raises(PTypeError):
        LineItem(sku="tea", qty="2", price_cents=350)


def test_global_invariant_runs_on_both_paths():
    with pytest.raises(InvariantException):
        Order(order_id="o1", lines=pvector(["not-a-line"]))

    valid = Order(order_id="o1", lines=pvector([ITEM]))
    with pytest.raises(InvariantException) as exc:
        valid.set(lines=pvector(["not-a-line"]))
    assert exc.value.invariant_errors == ("lines must contain LineItem records",)


def test_evolver_updates_meta_in_one_pass_and_shares_values():
    cart = freeze({"source": "web"})
    order = Order(order_id="o1", meta=freeze({"channel": "web", "cart": cart}))
    retagged = retag(order, channel="mobile", locale="en")

    assert retagged.meta["channel"] == "mobile"
    assert "locale" not in order.meta
    assert retagged.meta["cart"] is order.meta["cart"]


def test_structural_sharing_of_unchanged_elements():
    first, second = object(), object()
    base = pvector([first, second])
    extended = base.append(object())

    assert isinstance(extended, PVector)
    assert len(base) == 2 and len(extended) == 3
    assert extended[0] is first and extended[1] is second


def test_freeze_thaw_roundtrip():
    source = {"services": [{"name": "db", "port": 5432}], "debug": False}
    frozen = snapshot(source)
    source["debug"] = True

    assert isinstance(frozen, PMap)
    assert isinstance(frozen["services"], PVector)
    assert frozen["debug"] is False
    with pytest.raises(TypeError):
        frozen["debug"] = True

    plain = thaw(frozen)
    assert plain == {"services": [{"name": "db", "port": 5432}], "debug": False}
    assert type(plain) is dict
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.