class PhantomConflictError(ConflictError) A container names what it promises a reader, and reports back what it actually delivers:
cache = st.SortedMap(key=int) # monotonic atomic view, the default
ledger = st.SortedMap(key=int, isolation='snapshot')
books = st.SortedMap(key=int, isolation='serializable')
audit = st.SortedMap(key=int, isolation='strict_serializable')
cache.isolation # 'monotonic_atomic_view'
ledger.isolation # 'snapshot'
books.isolation # 'serializable'
audit.isolation # 'strict_serializable'Four names, refused by ValueError if misspelled — and the shipped type stubs turn a misspelling into a checker error before it ever runs.
Under snapshot every read a transaction makes is answered at the instant the transaction opened, so a repeated read returns what it first saw:
with st.transaction(ledger) as (view,):
first = view[account]
# ... another thread commits a new value for `account` here ...
assert view[account] == first # holds under snapshot, not under monotonicThat is what monotonic does not give you, and the reason to pay for the extra versions.
The cost is memory: a key keeps every version a live reader can still name, and the tail is freed once the last transaction closes.
Snapshot validates what a transaction wrote, plus any key it explicitly watched, and nothing more, so two transactions that each read what the other writes and then write elsewhere both land — write skew, which Snapshot Isolation permits by construction because no two writes ever collide. serializable validates the read set as well, which is also what makes a phantom detectable: a window a transaction scanned is remembered as a window, so a key committed into it afterwards is a conflict even though no key either transaction touched overlaps.
with st.transaction(books) as (view,):
empty = view.scan(20, 80) # reads a window, and records that it did
# ... another thread commits key 50 here ...
... # PhantomConflictError on leaving the blockPhantomConflictError derives from ConflictError, so a caller that retries on conflict needs no new branch. strict_serializable refuses exactly what serializable refuses; it adds only that a transaction opening after a commit returned cannot be ordered before it.
sharing decides how many writers can proceed at once:
st.SortedMap(key=int, sharing='locked') # one lock, the default
st.SortedMap(key=int, sharing='partitioned') # sixteen partitions, concurrent writersisolation reports what a container delivers, not what it was asked for, and sharding is the one place those differ:
sharded_snapshot = st.SortedMap(key=int, isolation='snapshot', sharing='partitioned')
sharded_monotonic = st.SortedMap(key=int, isolation='monotonic_atomic_view', sharing='partitioned')
sharded_snapshot.isolation # 'snapshot' — asked for snapshot, got snapshot
sharded_monotonic.isolation # 'read_committed' — asked for monotonic, got lessA monotonic container has no stamp for a reader to hold, so a walk crossing partitions can catch a commit half-applied, and only Read Committed survives above a single key. A snapshot container does have one — visibility there is a comparison against a stamp rather than a lock somebody holds — so its commits become visible everywhere at once and its level survives however many partitions it is spread across.
Keeping the guarantee is not free, though. Making a commit atomic across partitions costs in proportion to how many of them a transaction touches: one writing a single key is unaffected, while one writing across all sixteen serializes against every other transaction that does the same. Sharding pays off for point-heavy work spread over many keys, and least for transactions that touch everything.
locked is the default because it is the setting under which the default level means what it says.