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.1

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.1
File Size Uploaded
libtab-0.3.1.tar.gz 410.2 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for libtab 0.3.1
File
libtab-0.3.1-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.1-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.1-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.1-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: 789.3 kB

Release files / libtab-0.3.1.tar.gz

Download URL libtab-0.3.1.tar.gz
Size 410.2 kB
Tags Source
SHA-256 checksum
How to use checksums
afff5c2015f992a10db0bf513d76530b4abca4f6d02b195f15438f1ba29e6b5b
BLAKE2b-256 checksum
How to use checksums
81b753766c57d90b173330ec8a4d3ab0921cb8b8532244b1744df6fcd080b073
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.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.8 kB
Tags CPython 3.13 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
fb7712c004925e0c3f3cbdcbe32dab5a690d61a3393e623e628564e3a08cb6ca
BLAKE2b-256 checksum
How to use checksums
da29342a17d2544088286f6f904fbf5f795fa9fe560416489879f7ad5c9e4527
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.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.8 kB
Tags CPython 3.12 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
b8fa1f945228f4e8b80a4d41265695cbf39ddefa867e700f0ca6447fdaf3be23
BLAKE2b-256 checksum
How to use checksums
739101a167f19bfda18d519cb067ce30f20751e8de6ed627dc0b1b6459fa2ef1
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.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.8 kB
Tags CPython 3.11 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
22b9154014f4d3833473daadcb1132e5fcd673587a87304054eb448a24f7f5b7
BLAKE2b-256 checksum
How to use checksums
900da3c0b5037107d86618a53401dfea65cb91a5bfd96e14acbbbe94acbec1ac
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.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL libtab-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 94.8 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
2af92f5c39cf496ad63e65bc9bcadc4f482d1a46ff1c5543a0e5c40d4cbecede
BLAKE2b-256 checksum
How to use checksums
e24f453d45be1a5ff66ce8303859a144d11fbb862475f2de627222923917dd99
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

This release

0.3.1 This release

5 release files

0.3.0

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