Skip to main content

NimbusKV v3.1.0 README

Overview, architecture summary, installation, and usage as published on GitHub.

v3.1.0
Updated from GitHub 10/18/2026
Edit on GitHubView raw Markdown

PyPI Python Versions License: Apache 2.0 Tests Free-threading ready

Reactive, TTL-aware, concurrent key/value store for Python - lock-free reads always, one short lock per write, correct on both standard GIL Python and free-threaded Python (3.13t/3.14t).

Note

Free-threaded Python note: NimbusKV depends on immutables, whose C extension has not yet declared free-threading support, so importing it re-enables the GIL on 3.13t/3.14t. Run with PYTHON_GIL=0 (or python -X gil=0) to keep the GIL off. The NimbusKV test suite passes in that mode.

Note

Renamed from livedict. If you're upgrading: pip uninstall livedict && pip install nimbuskv, then change from livedict import LiveDict to from nimbuskv import NimbusKV. The API is otherwise unchanged.


What changed in v3 (rewrite, not a patch)#

v2 was a TTL dict with callbacks and a fake "sandbox" - a recombination of things cachetools/aiocache already do better. v3 is a different architecture:

  • Immutable-snapshot core - state lives as an immutable persistent snapshot (HAMT). Readers never lock, ever. Each write takes one short lock, builds the new snapshot from the current one and publishes it. (Up to v3.0.1 writers retried on conflict instead; under a hot key that turned into a retry storm, so it was replaced.)
  • Adapts to the interpreter automatically - same code path on standard GIL Python and free-threaded Python (3.13t/3.14t). No mode flag; detected once at import time.
  • Genuinely reactive - subscribe() pushes every change (set/delete/expire) to callbacks, per-key or global.
  • Real async, not fake async - for the in-memory path, aget/aset are truly non-blocking (no run_in_executor spent on work that was never I/O). When a persistence backend involves real I/O (SQLite/Redis), only that part is delegated to an executor.
  • atomic() replaces v2's manual lock()/unlock() for compound read-modify-write (e.g. counters). Your function runs exactly once, under the write lock, so keep it short and don't write to the same store from inside it.
  • Persistence is a write-through mirror, not the source of truth - Memory (default), SQLite, or Redis. Reads never touch the backend; it only exists for durability and startup rehydration.
  • mset()/mget() - batch multiple keys into a single atomic swap (readers never see a partial update across related keys), plus a convenience multi-key read.
  • wait_for() - block a thread until a condition on a key holds, driven by the subscription system, not polling. Built for producer/consumer coordination across threads.
  • Context manager support - with NimbusKV() as ld: guarantees the scheduler and backend are cleanly stopped, even on exception.
  • TTL scheduler bugfix - rescheduling a key's TTL now correctly invalidates the old deadline via a generation counter, instead of leaving a stale heap entry behind.
  • py.typed - ships as a properly typed package (PEP 561); type checkers (mypy/pyright) will pick up the annotations.
  • Sandbox dropped for this release. v2's "sandbox" was a timeout wrapper, not real isolation, and calling it a sandbox was misleading. It was dropped rather than shipping something that doesn't do what its name claims.

Install#

bash
pip install nimbuskv
pip install "nimbuskv[redis]" # only if you want the Redis backend

Works on Python 3.9+. For the lock-free write path, use a free-threaded build (3.13t/3.14t) - on standard GIL Python everything is still correct, just without the extra parallelism.


Quick usage#

python
from nimbuskv import NimbusKV
ld = NimbusKV() # in-memory, default
ld.set('foo', 123)
print(ld.get('foo')) # 123

TTL#

python
ld.set('temp', 'expire-me', ttl=2)

Reactive subscriptions#

python
def on_change(key, event, old_value, new_value):
print(key, event, old_value, '->', new_value)
sub = ld.subscribe(on_change) # all keys
sub_foo = ld.subscribe(on_change, key='foo') # just 'foo'
ld.set('foo', 1) # -> foo set None -> 1
ld.delete('foo') # -> foo delete 1 -> None
# TTL expiry fires its own 'expire' event, distinct from 'delete'

Atomic compound updates (replaces manual locking)#

python
ld.set('counter', 0)
ld.atomic('counter', lambda v: v + 1) # safe under concurrent writers, no lock held

Batch operations#

python
ld.mset({'a': 1, 'b': 2}) # one atomic swap for both keys
ld.mget(['a', 'b', 'missing']) # {'a': 1, 'b': 2, 'missing': None}

wait_for - thread coordination without polling#

python
import threading
def consumer():
result = ld.wait_for('job:status', lambda v: v == 'done', timeout=30)
print('unblocked with:', result)
threading.Thread(target=consumer).start()
# ... elsewhere, another thread ...
ld.set('job:status', 'done') # consumer's wait_for() returns right here

Context manager#

python
with NimbusKV() as ld:
ld.set('foo', 1)
# scheduler + backend cleanly stopped here, even if an exception was raised

Async (genuinely non-blocking for the in-memory path)#

python
import asyncio
from nimbuskv import NimbusKV
async def main():
ld = NimbusKV()
await ld.aset('bar', 'hello')
print(await ld.aget('bar'))
asyncio.run(main())

Persistence backends#

python
ld = NimbusKV(backend='sqlite:/path/to/store.db')
# or
ld = NimbusKV(backend='redis://localhost:6379/0')

State is rehydrated from the backend on startup (already-expired keys are skipped).


Architecture#

Architecture Diagram (Source)
graph TD
    A[NimbusKV public API]
    A --> B[ReactiveStore]
    B --> B1["in-memory, immutable-snapshot core"]
    B --> B2["readers: lock-free, always"]
    B --> B3["writers: one short lock,<br/>build + publish new snapshot"]
    A --> C[ExpiryScheduler]
    C --> C1["background thread, min-heap,<br/>fires TTL expiry against the store"]
    A --> D[PersistenceBackend]
    D --> D1["write-through mirror + startup rehydration"]
    D --> D2["NullBackend / SQLiteBackend / RedisBackend"]

Status#

v3.1.0 replaces the optimistic retry loop on the write path with a short lock and fixes several concurrency bugs (see CHANGELOG.md). Core, TTL, reactive subscriptions, atomic(), and all three persistence backends are implemented and tested for correctness (race-free under concurrent writers, verified with 10,000+ concurrent operations across 20 threads on standard Python - free-threaded benchmarks pending, to be published once run on a 3.14t interpreter). We are constantly improving and adding features to the library to make it better with every version.


Documentation#

Full detailed docs, versioned:


Development / Contributing#

bash
git clone https://github.com/NimbusKV/NimbusKV.git
cd NimbusKV
pip install -e ".[dev]"
pytest tests/ -v

See CONTRIBUTING.md for guidelines, and the examples/ folder for runnable end-to-end scripts. Free-threaded (3.13t/3.14t) testing is run in CI, but if you have a free-threaded interpreter locally, running the suite there too is especially valuable - that's the runtime this library's core claim is actually about.


License#

NimbusKV is licensed under the Apache License 2.0. You can use it freely, including in commercial and closed-source products. If you redistribute it, keep the LICENSE and NOTICE files.

License change: versions up to and including 3.0.1 were released under the GNU GPL v3.0. From 3.1.0 onwards, NimbusKV is Apache-2.0. Copies of 3.0.1 and earlier that you already have remain under the GPL v3.0.

Copyright 2026 Hemanshu Vaidya.


Support development#

If NimbusKV is useful to you, consider supporting development: 👉 NimbusKV on GitHub