Python
The namidb Python package is a PyO3 / maturin wrapper over the Rust
engine. Heavy lifting happens in Rust; the Python surface is thin and
explicit.
See Install for
the full install matrix and the pyarrow >= 14 requirement.
Client(uri)
import namidb
client = namidb.Client("memory://demo")The URI selects the backend (see Storage backends for the complete grammar). The first call per process bootstraps a tokio runtime owned by the client.
URI parsing is delegated to the same Rust implementation the CLI and
server use. Malformed URIs raise PyValueError; backend-init failures
raise PyRuntimeError.
Two write paths
Cypher (auto-commit)
client.cypher("CREATE (a:Person {name: 'Alice', age: 30})")client.cypher("CREATE (a:Person {name: 'Bob', age: 25})")
result = client.cypher( "MATCH (p:Person) WHERE p.age > $min RETURN p.name AS name, p.age AS age", params={"min": 26},)
print(result.columns) # ['name', 'age']print(len(result)) # 1print(result.first()) # {'name': 'Alice', 'age': 30}for row in result.rows(): print(row)Cypher writes (CREATE, SET, DELETE, MERGE, REMOVE) are
durably committed before cypher() returns — the executor calls
commit_batch() at the end of every write plan. You still want to
call client.flush() periodically to push the memtable into L0
SSTs, but durability is taken care of.
Typed staging API
import uuidimport namidb
client = namidb.Client("memory://demo")
alice = str(uuid.uuid7())bob = str(uuid.uuid7())
client.upsert_node("Person", alice, {"name": "Alice", "age": 30})client.upsert_node("Person", bob, {"name": "Bob"})client.upsert_edge("KNOWS", alice, bob, {"since": 2020})client.commit() # WAL + manifest CASclient.flush() # memtable -> L0 SSTsThe staging API stages mutations in the current batch and waits for
an explicit client.commit(). Pair with client.flush() when you
want the L0 SSTs on disk / in the bucket right away.
Tombstones use the same shape: client.tombstone_node(label, id),
client.tombstone_edge(edge_type, src, dst).
Bulk inserts
For thousands of nodes / edges in one round trip, prefer
merge_nodes / merge_edges — they batch many writes behind a single
tokio-runtime + mutex round trip:
client.merge_nodes( "Person", [{"id": str(uuid.uuid4()), "name": f"p{i}", "age": 20 + i} for i in range(10_000)],)client.merge_edges( "KNOWS", [ {"src": "uuid-a", "dst": "uuid-b", "since": 2020}, {"src": "uuid-b", "dst": "uuid-c", "since": 2021}, ],)client.commit()client.flush()Each node row needs an id (UUID string) plus arbitrary properties.
Each edge row needs src + dst (UUID strings) plus arbitrary
properties. merge_nodes / merge_edges stage into the current batch
— call commit() to make the mutations durable.
Async (acypher)
import asyncioimport namidb
async def main() -> None: client = namidb.Client("memory://demo") await client.acypher("CREATE (p:Person {name: 'Alice'})") result = await client.acypher( "MATCH (p:Person {name: $name}) RETURN p.name AS name", params={"name": "Alice"}, ) print(result.rows())
asyncio.run(main())acypher rides the pyo3-async-runtimes tokio bridge. Every call runs
on the same multi-threaded tokio runtime that backs the synchronous
API, so mixing the two from one Client is fine.
Result formats
QueryResult exposes the same rows in several shapes. pyarrow >= 14
is a hard dependency, so to_arrow() is always available:
result = client.cypher( "MATCH (p:Person) RETURN p.name AS name, p.age AS age ORDER BY p.age DESC")
result.rows() # list[dict] — easy for small resultstable = result.to_arrow() # pyarrow.Tabledf = result.to_pandas() # pandas.DataFrame (needs pandas extra)pl_df = result.to_polars() # polars.DataFrame (needs polars extra)Calling to_polars() without the polars extra raises a clear
ImportError pointing at the install command.
Column order follows the RETURN projection from the parsed plan, so
RETURN p.name AS name, p.age AS age always gives you columns
["name", "age"] — even when nothing matches.
For label-wide scans you can skip the Cypher round-trip:
table = client.scan_label_arrow("Person")# Columns: id, label, lsn, schema_version, then the union of property# keys across the scanned views (missing keys filled with None).Direct API
For lookups that don’t justify a Cypher round-trip:
client.lookup_node("Person", alice)# {'id': '...', 'label': 'Person', 'lsn': 1, 'schema_version': 0,# 'properties': {'name': 'Alice', 'age': 30}}
client.scan_label("Person")client.out_edges("KNOWS", alice)client.cache_stats() # {hit, miss, evict, ...} for the shared SstCacheType mapping (Cypher ↔ Python)
Cypher RuntimeValue | Python type |
|---|---|
Null | None |
Bool | bool |
Integer | int |
Float | float |
String | str |
Bytes | bytes |
Vector(Vec<f32>) | list[float] |
List | list |
Map | dict[str, ...] |
Date | datetime.date |
DateTime (UTC, μs) | datetime.datetime (tz=UTC) |
Node | {"_kind": "node", "id", "label", "properties"} |
Rel | {"_kind": "rel", "edge_type", "src", "dst", "properties"} |
Path | list[Node|Rel] alternating |
bool is checked before int so True / False do not silently
round-trip as Integer(1) / Integer(0).
What’s next
- Reading data — read clauses.
- Writing data — write clauses and auto-commit.
- Embedded (Python) — when and why to embed vs. talk to the HTTP server.
- HTTP API — talking to a
remote
namidb-serverfrom anywhere.