Skip to main content

cqlite-py

Python bindings for CQLite - a high-performance library for reading Apache Cassandra 5.0 SSTable files locally, without requiring a running Cassandra cluster.

Installation

pip install cqlite-py

Quick Start

import cqlite

# Open a database with schema
with cqlite.open('path/to/sstables', schema='schema.cql') as db:
    # Execute queries
    for row in db.execute('SELECT * FROM keyspace.table LIMIT 10'):
        print(row.to_dict())

Features

  • Zero cluster dependency - Read SSTable files directly from disk
  • Full CQL type support - All primitive types, collections, UDTs, and frozen types
  • Memory-efficient streaming - Iterate over large datasets without loading all rows
  • Thread-safe database handles - Safe concurrent access from multiple threads
  • Cross-platform - Linux (x86_64, ARM64), macOS (Intel, Apple Silicon), Windows

Supported Platforms

Platform Architecture Status
Linux x86_64 ✅
Linux ARM64 ✅
macOS Intel (x86_64) ✅
macOS Apple Silicon ✅
Windows x64 ✅

Requirements

  • Python 3.9+ — the boundaries are CI-tested, not merely advertised. CI installs the abi3 wheel on 3.9 (the floor), 3.14 (the current top of the range) and 3.13 (an additional compatibility leg), running an import plus one real query against the canonical corpus on each (Floor smoke in .github/workflows/python-ci.yml, issue #1459).

    That check is a binding matrix tier, so per repo CI-cost policy it runs on every push to main, on the nightly schedule, and on PRs labeled ci:bindings-full — not on a routine unlabeled PR. main is therefore continuously floor-tested and releases are cut from it, but a floor break is caught just after merge rather than before it.

    The range is unbounded above, so its top is the newest released CPython — when 3.15 ships it belongs in that matrix.

  • Cassandra 5.0 SSTable files

API Reference

Opening a Database

import cqlite

# Context manager (recommended)
with cqlite.open(data_dir, schema=schema_path) as db:
    # use db...

# Manual management
db = cqlite.open(data_dir, schema=schema_path)
# use db...
db.close()

A handle that is garbage-collected without close() still cleans up best-effort: the write engine is closed (flushing any remaining memtable to an SSTable), the read engine's shutdown hook is called (today a no-op), and buffered telemetry is flushed. Some things are worth knowing about relying on it:

  • It runs with the GIL held, because CPython frees the object from its deallocator. The flush and fsync therefore block other Python threads for their duration, where close() releases the GIL around the same work. Prefer with or an explicit close() in threaded code.
  • It is best-effort by design. If the handle is dropped from inside a running Tokio runtime context — CQLite driven from a Rust async host — the cleanup is skipped rather than risk an unsafe teardown, so unflushed rows stay in the write-ahead log (replayable) instead of being flushed. close() is the only path with a guarantee.
  • A Python asyncio event loop is NOT such a context. These bindings have no asyncio integration, so an asyncio thread has no Tokio runtime and nothing is skipped: a handle collected there runs the full flush + fsync with the GIL held, blocking the event loop (and up to ~5s more if OpenTelemetry export is enabled and the collector is unreachable). In asyncio code, close handles explicitly — ideally off the loop thread.

A StreamingIterator that outlives its Database is unaffected by the handle being collected, for both read-only and writable handles: it keeps yielding its remaining rows. Its rows come from a background task holding its own reference to the storage engine, so dropping the handle cannot stop the stream, and this cleanup deliberately does not invalidate it. (An explicit close() still does — that is a user stating intent, and is unchanged; see issue #1462.)

Executing Queries

# Simple query
results = db.execute('SELECT * FROM keyspace.table')
for row in results:
    print(row.to_dict())

# With LIMIT
for row in db.execute('SELECT name, age FROM users LIMIT 100'):
    print(f"{row['name']}: {row['age']}")

# Access query metadata
print(f"Rows returned: {len(results)}")
print(f"Execution time: {results.execution_time_ms}ms")
print(f"Columns: {[col.name for col in results.columns]}")

Streaming Large Results

For memory-efficient iteration over large datasets:

from cqlite import StreamingConfig

# Configure streaming for memory efficiency
config = StreamingConfig(buffer_size=512, chunk_size=1000)
for row in db.execute_streaming('SELECT * FROM large_table', config=config):
    process(row)

# Track progress
iterator = db.execute_streaming('SELECT * FROM large_table')
for row in iterator:
    if iterator.rows_received % 10000 == 0:
        print(f"Processed {iterator.rows_received} rows")

Configuration Presets

import cqlite

# Built-in presets for common use cases
config = cqlite.memory_optimized()      # 256 MB max memory
config = cqlite.performance_optimized() # 4 GB max memory

# Open database with preset configuration
db = cqlite.open('path/to/data', schema='schema.cql', config='memory_optimized')

# Validate custom configuration
custom_config = {'memory': {'max_memory': 536870912}}  # 512 MB
cqlite.validate_config(custom_config)

Refreshing SSTables (v0.13)

If Cassandra (or another process) writes new SSTables while your database handle is open, call refresh() to re-discover them. Refresh is explicit-only (CQLite never rescans behind your back) and atomic / fail-closed: if any newly found generation fails to open, the swap is rolled back and the handle keeps serving the prior, consistent set of readers.

import cqlite

with cqlite.open('path/to/sstables', schema='schema.cql') as db:
    # ... time passes; Cassandra flushes/compacts new SSTables to disk ...

    report = db.refresh()
    print(f'Tables scanned:  {report.tables_scanned}')
    print(f'Readers added:   {report.readers_added}')
    print(f'Readers removed: {report.readers_removed}')

    # Subsequent queries see the newly discovered data
    for row in db.execute('SELECT * FROM keyspace.table'):
        print(row.to_dict())

refresh() returns a RefreshReport with the integer attributes tables_scanned, readers_added, and readers_removed, plus a to_dict() helper. It raises RuntimeError if the database is already closed, and CqliteError if a newly discovered generation fails to open (the prior reader set is preserved).

Result Byte Budget (v0.13)

Non-streaming execute() queries are bounded by a result-size budget, defaulting to 64 MiB (64 * 1024 * 1024 bytes). When the materialized result's running byte estimate exceeds the budget, the query fails with a cqlite.QueryError directing you to add a LIMIT clause or switch to execute_streaming(). Streaming queries are not subject to this budget.

Adjust the budget with the max_result_bytes key in the config passed to cqlite.open() (absent, it stays at 64 MiB):

import cqlite

# Raise the budget to 256 MiB for this handle
config = {'max_result_bytes': 256 * 1024 * 1024}

try:
    with cqlite.open('path/to/data', schema='schema.cql', config=config) as db:
        rows = db.execute('SELECT * FROM keyspace.big_table')
        for row in rows:
            process(row)
except cqlite.QueryError as e:
    # Result exceeded the byte budget — add a LIMIT or stream instead
    print(f'Result too large: {e}')
    for row in db.execute_streaming('SELECT * FROM keyspace.big_table'):
        process(row)

OpenTelemetry Tracing (v0.13)

CQLite can emit OpenTelemetry traces when built with the observability Cargo feature; without that feature the configuration is accepted but is a no-op. Pass an otel_config dict to cqlite.open(). Values are layered over the CQLITE_OTEL_* environment variables, and OpenTelemetry is initialized once per process.

import cqlite

db = cqlite.open(
    'path/to/sstables',
    schema='schema.cql',
    otel_config={
        'enabled': True,                       # default False
        'endpoint': 'http://localhost:4317',   # default 'http://localhost:4317'
        'protocol': 'grpc',                    # 'grpc' (default) or 'http'
        'service_name': 'cqlite',              # default 'cqlite'
        'service_version': '0.13.0',           # default: package version
        'sampling_ratio': 1.0,                 # default 1.0
        'timeout_ms': 10000,                   # default 10000
    },
)

Unknown keys raise ValueError.

Error Handling

import cqlite

try:
    with cqlite.open('path/to/data', schema='schema.cql') as db:
        result = db.execute('SELECT * FROM keyspace.table')
        for row in result:
            print(row.to_dict())
except cqlite.ParseError as e:
    print(f"Query syntax error: {e}")
except cqlite.QueryError as e:
    print(f"Query execution failed: {e}")
except cqlite.SchemaError as e:
    print(f"Schema validation failed: {e}")
except IOError as e:
    print(f"File not found: {e}")
except RuntimeError as e:
    print(f"Database already closed: {e}")

Exception Hierarchy:

CqliteError (base exception)
├── SchemaError   - Schema parsing or validation failures
├── QueryError    - Query execution failures
└── ParseError    - CQL syntax errors

Built-in exceptions also used:
├── IOError       - File system errors
├── ValueError    - Invalid configuration
├── RuntimeError  - Invalid state (e.g., database closed)
└── MemoryError   - Memory allocation failures

Type Conversions

CQL types are automatically converted to Python native types:

CQL Type Python Type
text, varchar str
int, bigint, smallint, tinyint int
float, double float
boolean bool
blob bytes
timestamp datetime.datetime
date datetime.date
time int (nanoseconds since midnight, lossless)
duration cqlite.Duration (exact months / days / nanos)
uuid, timeuuid uuid.UUID
inet ipaddress.IPv4Address or IPv6Address
decimal decimal.Decimal
varint int (arbitrary precision)
list<T> list
set<T> frozenset
map<K,V> dict
tuple<...> tuple
frozen<T> Unwrapped inner type
UDT cqlite.Udt — .type_name / .keyspace / .fields (see below)

UDT type identity is carried out of band

A CQL user-defined type decodes to a cqlite.Udt:

udt = row["address"]
udt.type_name          # 'address_type'  — the declared UDT type
udt.keyspace           # 'test_collections'
udt.fields             # mappingproxy({'street': '1 Main St', 'city': 'SF'}) — declared fields ONLY
udt["street"]          # mapping access, delegating to .fields
"city" in udt, len(udt), sorted(udt.keys())

Breaking change (issue #3504). _type and _keyspace used to be injected as dict keys, i.e. into the same namespace as the UDT's own field names — so a UDT declaring a field named _type or _keyspace (legal CQL via a quoted identifier) silently overwrote the marker and the type name became unrecoverable. Migration:

Before Now
udt["_type"] udt.type_name
udt["_keyspace"] udt.keyspace
udt["street"] udt["street"] (unchanged) or udt.fields["street"]
isinstance(v, dict) to spot a UDT isinstance(v, cqlite.Udt)

udt["_type"] now reaches a FIELD of that name, raising KeyError when the UDT declares none. Udt is frozen, and equality/hashing are over (keyspace, type_name, fields), so it can be used as a dict key whenever its field values are hashable. udt.fields is therefore a read-only types.MappingProxyType view: udt.fields["z"] = 1 raises TypeError rather than moving a Udt already used as a key out of its hash bucket. Take dict(udt.fields) for a mutable copy.

CQL decimal rendering policy

A CQL decimal is unscaled x 10^(-scale) where unscaled is an arbitrary-precision two's-complement integer. Both CQLite language bindings share one implementation and one policy (issue #1452), so a value can never render in one binding and be refused by the other:

Condition Outcome
Unscaled magnitude > 32 KiB Refused as corrupt: a typed error naming the scale, the unscaled length and the ceiling
Magnitude > 1024 bytes, or abs(scale) > 1_000_000 Precision-preserving exponent form, <digits>e<-scale> — every digit exact
scale < 0 — a legal and common Cassandra encoding Exponent form at any magnitude, independent of the thresholds above: e.g. unscaled = 123, scale = -2 renders 123e2
Otherwise (scale >= 0) Positional form, e.g. 1.23, 0.00123, -0.123

A negative scale multiplies by a power of ten, so there is no positional form for it and none of the size thresholds apply. A consumer that parses these strings must therefore accept exponent form at any magnitude: ^-?[0-9]+(\.[0-9]+)?$ alone is not sufficient.

Below the 32 KiB ceiling the render is infallible: a well-formed value always renders, whatever its scale. Above it the refusal is a typed, catchable error — a corrupt SSTable never aborts the host process.

Write Operations

CQLite v0.9.0 adds write support to the Python bindings. Open the database with writable=True and a write_dir to enable write operations.

import cqlite

with cqlite.open(
    'path/to/sstables',
    schema='schema.cql',
    writable=True,
    write_dir='/tmp/my-writes',
) as db:
    # Write rows via CQL INSERT, UPDATE, or DELETE
    db.execute(
        "INSERT INTO test_basic.simple_table (id, name, age) "
        "VALUES (11111111-1111-1111-1111-111111111111, 'Alice', 30)"
    )
    db.execute(
        "UPDATE test_basic.simple_table SET age = 31 "
        "WHERE id = 11111111-1111-1111-1111-111111111111"
    )

    # Flush the in-memory write buffer (memtable) to an SSTable on disk.
    # Returns the path to the flushed Data.db file.
    path = db.flush_run()
    print(f'Flushed to: {path}')

    # Run background compaction within a time budget
    report = db.maintenance_step(budget_ms=100)
    print(f'Merged {report.rows_merged} rows in {report.time_spent_ms:.1f} ms')
    if report.pending_compaction:
        print('More compaction work available')

    # Inspect write statistics
    stats = db.write_stats
    print(f'Memtable size: {stats.memtable_size_bytes} bytes')
    print(f'Total flushed: {stats.total_written_bytes} bytes')

Write API

Method / Property Description
db.execute(cql) Execute a CQL INSERT, UPDATE, or DELETE statement
db.flush_run() Flush memtable to SSTable; returns the Data.db path or "" if memtable was empty
db.maintenance_step(budget_ms) Run STCS compaction for up to budget_ms milliseconds; returns MaintenanceReport
db.write_stats WriteStats property: memtable_size_bytes, memtable_row_count, total_written_bytes, l0_sstable_count

Known Limitations

  • Counter columns cannot be written — execute() raises CqliteError for counter mutations.
  • Concurrent queries on the same handle may need a warm-up query first (Issue #311).

See docs/write-support-limitations.md for the full limitations reference.

Resources

License

MIT OR Apache-2.0

Metadata

Release files for cqlite-py 0.18.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cqlite-py 0.18.0
File Size Uploaded Provenance Actions
cqlite_py-0.18.0.tar.gz 7.2 MB With provenance Details for cqlite_py-0.18.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for cqlite-py 0.18.0
File Actions
cqlite_py-0.18.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details for cqlite_py-0.18.0-cp39-abi3-win_amd64.whl
cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_x86_64.whl CPython 3.9 abi3 Linux glibc 2.28+ x86-64 Details for cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_x86_64.whl
cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_aarch64.whl CPython 3.9 abi3 Linux glibc 2.28+ ARM64 Details for cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_aarch64.whl
cqlite_py-0.18.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details for cqlite_py-0.18.0-cp39-abi3-macosx_11_0_arm64.whl
cqlite_py-0.18.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details for cqlite_py-0.18.0-cp39-abi3-macosx_10_12_x86_64.whl

Total release size: 31.9 MB

Release files / cqlite_py-0.18.0.tar.gz

Download URL cqlite_py-0.18.0.tar.gz
Size 7.2 MB
Tags Source
SHA-256 checksum
How to use checksums
625947b25b5927407d738ebf4141d2648da3a4ba9cac82810dfb380389a594a9
BLAKE2b-256 checksum
How to use checksums
3fdd7f26e333d500f18958955ce8215519c439981e22b9dd0a841cbd4c959abd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / cqlite_py-0.18.0-cp39-abi3-win_amd64.whl

Download URL cqlite_py-0.18.0-cp39-abi3-win_amd64.whl
Size 5.2 MB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
25fad9e066a24e50dec5bfae3e802f96ec5e3b60ec0835a29f42d44951c8ecf0
BLAKE2b-256 checksum
How to use checksums
fded93d53727b284c6cb3846fe090814e99fb7d31cf3514afdcfd4c09c51e368
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_x86_64.whl

Download URL cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_x86_64.whl
Size 5.1 MB
Tags CPython 3.9 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
36c63e30b986a683903fa7f7686d14ffecd15a49875954c62b86300a96017b06
BLAKE2b-256 checksum
How to use checksums
0e194da46fc8b693ab34204010f780e64463499b6cb7d8e7891578375938a565
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_aarch64.whl

Download URL cqlite_py-0.18.0-cp39-abi3-manylinux_2_28_aarch64.whl
Size 4.8 MB
Tags CPython 3.9 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
fd6ef94acabc6e6548e1e7d256c9e7987a808216f5bd36d9b277efd46226e49c
BLAKE2b-256 checksum
How to use checksums
6efe9410bc2d63bc106f549b8186e8f98fdf00441eb4927f8752b4a0a7a55b2d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / cqlite_py-0.18.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL cqlite_py-0.18.0-cp39-abi3-macosx_11_0_arm64.whl
Size 4.6 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
5fa36bb5486f95ac5c6b7386c3244db24ad4060ade8a685a77f8344b9c51424d
BLAKE2b-256 checksum
How to use checksums
f9cd99e9f03e3071a06f4f435e7d24b9cba811a545115ae7155f155a0e4fffd6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / cqlite_py-0.18.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL cqlite_py-0.18.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 5.0 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
d54b2987eae9585b3181f78f5fbb262436d0d814cab396b7eab4bc82333e966d
BLAKE2b-256 checksum
How to use checksums
a5ca3d02ffc615c4ec6243c860d94220ed27db87a937c516979286255469612b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Security for cqlite-py 0.18.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 6 files in this release have been uploaded with provenance metadata.

Select from 6 files

Attestation for cqlite_py-0.18.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page