Skip to main content

py-libtab

A lightweight, database-like store — without the database engine. SQLite gave you a relational DB in a file; py-libtab gives you a document store in files, and the files stay human-readable.

py-libtab writes and reads .tab files: ordinary, human-readable text files that behave like a small document/NoSQL collection. Each file carries a declared, typed schema (the columns live in the file itself — nothing external to keep in sync), rows in a deterministic order (so the same data serializes byte-for-byte the same, and files diff cleanly in git), and, when you want it, per-field cryptography — a field can be hashed, signed, or sealed (encrypted), with the algorithm and parameters riding inline in the cell. It's just a file: cat it, grep it, git diff it, back it up with cp.

Script it, move it, keep secrets in it — three things you can't do all at once with a plain text file:

  • Script it with the usual Unix tools. The schema and non-secret rows are ordinary text, so grep, awk, cut, sed, and git diff work on them directly. (Sealed values are ciphertext by design — you can grep the labels and structure, not the secrets.)
  • Move it anywhere as one self-describing file. Schema, and each cell's crypto algorithm/parameters, ride inside the file — no external schema, no engine, no server. cp/scp/rsync it; it's the same on disk, in a backup, in git. Install is a single pip install libtab with prebuilt wheels — no compiler. (Wheels currently target Linux x86_64.)
  • Keep secrets in it. A field can be sealed (XChaCha20-Poly1305, with the nonce and MAC inline), so the value stays confidential at rest while the rest of the table stays readable — a greppable file that still holds real secrets.

Why this exists

If you want structured data on disk from Python, your options are usually:

  • a database — SQLite or a server: real queries, but an opaque binary and an engine to run; you can't grep it or git diff it;
  • a document store (Mongo & friends) — the document shape you want, but your documents are locked inside the engine, not files you can open;
  • JSON/CSV — real files, but no declared schema, no stable ordering, and if you need crypto you bolt it on by hand (and probably get it subtly wrong).

There's no lightweight middle: a document store whose documents are real, readable files — schema and ordering built in, crypto available per field. That's the gap py-libtab fills. It's a store and a format, not a query engine: you get attribute lookup and search over a table, not joins, indexes, or cross-collection transactions — that's the point of having no engine.

Three ways to protect a field

Type Guarantees Readable? Use for
HASHED integrity — irreversible digest the digest is checksums, content addresses, "did this change?"
SIGNED authenticity — tamper-evident yes, plaintext + signature audit logs, signed config/release metadata
sealed confidentiality — encrypted no, only the key holder API keys, tokens, anything secret at rest

Good fits: tamper-evident audit logs whose entries you can still read · signed release metadata · content-addressable manifests · API-key / token vaults where the values are sealed but the surrounding table (ids, labels, which key is which) stays greppable.

One caveat worth stating: HASHED and SIGNED cells are readable by design — a signed value's plaintext is right there in the file. Use sealed for anything that must stay secret. (And note a bare password-hash table, even committed as HASHED, is still an offline-cracking target if it leaks — for credentials you'd typically seal the table or keep it out of version control.)

How it works

                              py-libtab
   ══════════════════════════════ WRITE ══════════════════════════════

     your Python code  —  writing a tamper-evident audit log
     ┌──────────────────────────────────────────────────────────────┐
     │ t = Tabula.create("audit.tab", ...)                           │
     │ t.set(row, "event", "user.delete")   → plain text             │
     │ t.set_hashed(row, "payload", blob)   → BLAKE2b content hash    │
     │ t.set_signed(row, "sig", entry, sk)  → Ed25519 signature      │
     │ t.commit()                                                    │
     └───────────────┬──────────────────────────────────────────────┘
                     │ calls
                     ▼
     ┌──────────────────────────────────────────────────────────────┐
     │  native library   (audited C crypto — text parser ·          │
     │  BLAKE2b · argon2id · Ed25519 · XChaCha20-Poly1305)          │
     └───────────────┬──────────────────────────────────────────────┘
                     │ writes
                     ▼
   ┌─────────────────────────────────────────────────────────────────┐
   │  audit.tab   —  plain text: cat it · grep it · diff it in git    │
   │                                                                   │
   │   schema=audit                                                    │
   │       col=id                                                      │
   │       col=event                                                   │
   │       col=payload type=HASHED                                     │
   │       col=sig     type=SIGNED signer=ci                           │
   │                                                                   │
   │   id=2024-06-01T12:00Z-0001                                       │
   │       event=user.delete                                           │
   │       payload=hashed:AQ8Kf3...      ← self-describing: the algo,  │
   │       sig=signed:aGVsbG8=:3q2+7w...    params & salt ride inline  │
   └─────────────────────────────────────────────────────────────────┘
                     │
   ══════════════════╪═══════════ READ + VERIFY ══════════════════════
                     │ reads
                     ▼
     ┌──────────────────────────────────────────────────────────────┐
     │ t = Tabula.open("audit.tab")                                  │
     │ row = t.search("event", "user.delete")[0]                     │
     │                                                               │
     │ t.verify_hash(row, "payload", blob)  → True / False  (did the │
     │                                        payload change?)        │
     │ t.verify_signed(row, "sig", pubkey)  → entry bytes, or raises │
     │                                        if the entry was forged │
     └──────────────────────────────────────────────────────────────┘

   one representation everywhere:  on disk == on the wire == in a backup
   the file IS the data · the crypto rides inside it · you can still read it

Usage

Write a tamper-evident audit log. Each entry records an event, a content hash of its payload (integrity), and an Ed25519 signature over the entry (provenance):

from libtab import Tabula, Column

t = Tabula.create("audit.tab", "audit", [
    Column("id"),
    Column("event"),
    Column("payload", type="HASHED"),
    Column("sig", type="SIGNED", signer="ci"),
])
row = t.add_row("id", "2024-06-01T12:00Z-0001")
t.set(row, "event", "user.delete")
t.set_hashed(row, "payload", payload_blob)     # BLAKE2b digest of the payload
t.set_signed(row, "sig", entry_bytes, sk)      # sk = your 32-byte signing key
t.commit()
t.close()

The file on disk is just text — greppable, diffable in git, and every entry carries its own verification data:

schema=audit
	col=id
	col=event
	col=payload type=HASHED
	col=sig type=SIGNED signer=ci

id=2024-06-01T12:00Z-0001
	event=user.delete
	payload=hashed:AQ8Kf3...
	sig=signed:aGVsbG8=:3q2+7w...

Read it back and verify — the hash proves the payload wasn't altered, the signature proves the entry is authentic (and returns the signed bytes, or raises if it was forged):

from libtab import Tabula

t = Tabula.open("audit.tab")
row = t.search("event", "user.delete")[0]

t.verify_hash(row, "payload", payload_blob)    # True if the payload matches
entry = t.verify_signed(row, "sig", pubkey)    # signed bytes, or raises
t.close()

Sealing a secret

For values that must stay confidential, set_sealed encrypts the field under a 32-byte key; only the key holder can read it back with get_sealed. The surrounding table stays plain text, so you can still grep for a row — but the sealed value is ciphertext on disk:

import os
from libtab import Tabula, Column

key = os.urandom(32)   # keep this out of the file

t = Tabula.create("vault.tab", "vault", [
    Column("id"),
    Column("secret"),        # an ordinary column; sealing is per-value
])
row = t.add_row("id", "stripe-api-key")
t.set_sealed(row, "secret", b"sk-live-abc123", key)
t.commit()
t.close()
# on disk:  secret=sealed:LZ5tJO8m...   (plaintext never present)

t = Tabula.open("vault.tab")
row = t.search("id", "stripe-api-key")[0]
t.get_sealed(row, "secret", key)              # b"sk-live-abc123"
t.get_sealed(row, "secret", os.urandom(32))   # raises — wrong key or tampered
t.close()

Sealing (XChaCha20-Poly1305) is length-preserving, like any AEAD: the ciphertext hides the value but not its size, so the length of a sealed secret is inferable from the file. If that matters for your threat model, pad the plaintext to a fixed size before sealing.

Sealing uses XChaCha20-Poly1305 with a fresh random nonce per value, so it's also tamper-evident: a modified ciphertext fails to decrypt. The raw seal(key, data) / unseal(key, blob) functions are exposed too if you want to encrypt something without a table.

The .tab format

A simple attribute/value text file. The first tuple declares the schema; every later tuple is a row keyed by the first column:

schema=orders
	col=id
	col=item
	col=qty

id=a
	item=widget
	qty=5

id=b
	item=gadget
	qty=3

Columns may be typed HASHED (BLAKE2b or argon2id) or SIGNED (Ed25519); typed cells carry a self-describing <type>:<base64url> tag that includes the algorithm, parameters, and salt, so a cell is verifiable on its own. sealed cells (XChaCha20-Poly1305) use the same tag convention on an ordinary column. Untyped columns are plain text.

The crypto is the real thing — BLAKE2b, argon2id, Ed25519, and XChaCha20-Poly1305 from an audited C implementation, not hand-rolled hashlib calls with guessed-at parameters.

Build & install

Requires a C toolchain (gcc, ar).

cd vendor && ./build.sh          # builds the native library
cd .. && pip install .

The engine is compiled from vendored C sources; pip install links it in. There are no Python runtime dependencies.

Testing

These files carry secrets, integrity data, and provenance people rely on, so py-libtab is tested like the security primitive it is: unit + integration tests, fuzzing of the parser and the decrypt path under AddressSanitizer, crypto known-answer vectors (BLAKE2b/argon2id/Ed25519/XChaCha20-Poly1305 checked against independent references), and mutation testing (0 surviving mutants). One command runs everything:

./run-all-tests.sh          # fast gate
./run-all-tests.sh all      # everything, incl. fuzz + mutation

See TESTING.md for the full breakdown of each layer.

Release files for libtab 0.3.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 libtab 0.3.0
File Size Uploaded
libtab-0.3.0.tar.gz 408.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for libtab 0.3.0
File
libtab-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.13 CPython 3.13 Linux glibc 2.17+ x86-64 Details
libtab-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.12 CPython 3.12 Linux glibc 2.17+ x86-64 Details
libtab-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.11 CPython 3.11 Linux glibc 2.17+ x86-64 Details
libtab-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 CPython 3.10 Linux glibc 2.17+ x86-64 Details

Total release size: 787.0 kB

Release files / libtab-0.3.0.tar.gz

Download URL libtab-0.3.0.tar.gz
Size 408.6 kB
Tags Source
SHA-256 checksum
How to use checksums
118a38d3142fab3ae3b624fc4412ecff39921211863b3a9ee451baae578c6ebf
BLAKE2b-256 checksum
How to use checksums
769f51fedac00918c1260bde7c9427e8062407d70f0d7bc2edf33f74c9cca816
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 11, 2026.

Transparency log

Release files / libtab-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.6 kB
Tags CPython 3.13 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
64874be3d786d63bfcd0aac94e77e9c470e11b15450167171ae2f4a8c5f817f5
BLAKE2b-256 checksum
How to use checksums
7c38e6b8840c26611509b2ebfe15782a22bd8ba95e8f83be69f5098485b3faa7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 11, 2026.

Transparency log

Release files / libtab-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.6 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
5f9f0bbf4df822a1da5d9e6462a6f3536406a9009261b866d2447a25eac0ef4c
BLAKE2b-256 checksum
How to use checksums
40072c247245071dd6fa581132161ed563d757afa170d0a880fc6708e516db69
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 11, 2026.

Transparency log

Release files / libtab-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.6 kB
Tags CPython 3.11 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
a2b7663a0270d1329651e2835ccf3fb4e6bdd16e285601af626385448a96f00d
BLAKE2b-256 checksum
How to use checksums
3de0817a01906ca9bcfc58630a3ce2b9db44b66f86bbfc39c9f3c6fd1cce2899
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 11, 2026.

Transparency log

Release files / libtab-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.6 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
788276692902a5364bac147254d87e4260543e53dbaec1996c2edccc5a407e51
BLAKE2b-256 checksum
How to use checksums
8d8ec25d1cb8eda2bc13eca1c300541f1073a3d748237581abb01b9f40f53eae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 11, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.1

5 release files

This release

0.3.0 This release

5 release files

0.2.0

5 release files

0.1.0

5 release files

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