Traditional libraries optimise decoding speed. Rugo optimises reading less data in the first place — pushing column projection and predicate pushdown ahead of decode, so data you don't want is never touched. Extracted from years of work on the Opteryx SQL engine.
Often it comes from avoiding decoding data you don't need in the first place. Rugo was built as the file layer for the Opteryx SQL engine, where the goals were simple: read as little data as possible, hold as little in memory as possible. Column projection and row-group pruning happen before any decoding — so the columns and rows a query doesn't need are never paid for. The result is a library that gets faster the more selective your query is, stays tiny on disk, and carries no PyArrow, Pandas, or NumPy into your environment.
| Metric | Rugo | PyArrow |
|---|---|---|
| Installed footprint | 7.7 MB | 126 MB |
| Runtime dependencies | zero | Arrow C++ runtime |
| Cold import time | 7 ms | 27 ms |
| Schema read (footer only) | 0.02 ms | 0.03 ms |
Measured with Rugo 0.4.10 and PyArrow 25.0.0 on Python 3.14.5t, Apple M-series (macOS arm64 wheels). Import times on a cold process (first load off disk), best of 10. Footprint is the on-disk size of the installed package; neither library pulls a runtime dependency.
AWS Lambda and GCP Cloud Functions bill by memory and package size. A 7 ms cold import versus PyArrow's 27 ms adds up fast at scale-out.
At 16× smaller than PyArrow, rugo shrinks Lambda deployment packages and container images, speeds up deploys, and stays well clear of AWS Lambda's 250 MB unzipped layer limit.
Column projection and row-group pruning are first-class citizens. Skip the columns you don't need. Skip the row groups that can't match. The more selective the query, the bigger rugo's advantage.
The wheel bundles everything it needs — Draken, the
columnar substrate, ships inside.
pip install rugo is the entire
dependency story.
Where PyArrow is faster: full-table scans with no filtering. PyArrow's Parquet engine has had over a decade of optimisation poured into raw decode throughput — Rugo doesn't compete there. It competes on how little it has to decode in the first place.
Rugo is the file layer for applications that read data selectively rather than scan it wholesale — places where footprint, cold-start time, and memory matter as much as throughput.
Rugo reads and writes Parquet, CSV, and JSONL. The API is the same shape across all three: pass a path or bytes, get columnar data back.
pip install rugo
from rugo import parquet # Read — context manager, yields Morsels with parquet.read_parquet("planets.parquet", columns=["id", "name"], predicates=[("id", ">", 4)]) as reader: for morsel in reader: print(morsel.column("name").to_pylist()) # Metadata — no column decode meta = parquet.read_metadata("planets.parquet") print(meta.num_rows, [c.name for c in meta.schema_columns]) # Write data = parquet.write_parquet(morsel, compression="zstd")
from rugo import csv # Same shape — context manager, yields Morsels with csv.read_csv("data.csv", columns=["name"], predicates=[("age", ">", 30)]) as reader: for morsel in reader: print(morsel.column("name").to_pylist()) meta = csv.read_metadata("data.csv") data = csv.write_csv(morsel)
from rugo import jsonl # Same shape again with jsonl.read_jsonl("events.jsonl", columns=["id", "status"], predicates=[("status", "==", "active")]) as reader: for morsel in reader: print(len(morsel), "rows matched") meta = jsonl.read_metadata("events.jsonl") data = jsonl.write_jsonl(morsel)
The same install puts a rugo command on your
PATH — the reader and writer, driven from the
shell. Inspect, convert, and reshape files without writing
any Python.
# Look at a file rugo info events.parquet # rows, columns, size, format rugo schema events.parquet # column names, types, nullability rugo preview -n 5 events.parquet # first rows as a table rugo describe events.parquet # per-column null counts, min/max
# Reshape and move data between formats rugo convert events.parquet events.jsonl # format inferred from extensions rugo merge part-*.parquet all.parquet # concatenate schema-identical files rugo split --rows 100000 big.parquet # chunk into smaller files rugo diff before.parquet after.parquet # schema differences
# Every verb takes --json, so it composes with jq and shell pipelines rugo count --json events.parquet | jq .num_rows
A complete worked example on a real dataset is available in Google Colab or as a notebook on GitHub.
All three formats support column projection and predicate filtering. Writers produce standard output readable by any compliant tool.
read_parquet(source, columns, predicates) → context manager, one Morsel per row groupsource: str | bytescolumns: list[str] = Nonepredicates: list[tuple] = None(col, op, val) — ops: == != < <= > >= in "not in". Row-group pruning then row-level filter.read_metadata(source) → ParquetMetadatasource: str | byteswrite_parquet(morsel, compression, bloom_filters, max_rows_per_row_group) → bytescompression: str"zstd" or "none"bloom_filters: bool | list[str]max_rows_per_row_group: int0 for a single row groupDecode: int32/64, float32/64, boolean, byte_array · UNCOMPRESSED, SNAPPY, ZSTD · PLAIN, RLE_DICTIONARY, DELTA_BINARY_PACKED, DELTA_BYTE_ARRAY
Write: INT8–64, FLOAT32/64, BOOL, VARCHAR, DATE32, TIME, TIMESTAMP64, INTERVAL, DECIMAL, ARRAY — ZSTD or uncompressed, per-column min/max stats, optional bloom filters.
Rugo's Parquet advantage is reading less, not raw scan throughput. Row-group pruning and column projection reduce what is decoded; PyArrow's bulk scan is faster when all columns and rows are needed.
from rugo import parquet # Metadata only — no column decode meta = parquet.read_metadata("planets.parquet") print(meta.num_rows) print([c.name for c in meta.schema_columns]) # Selective streaming read with parquet.read_parquet( "planets.parquet", columns=["id", "name"], predicates=[("id", ">", 4)], ) as reader: for morsel in reader: names = morsel.column("name").to_pylist() # Write (ZSTD default) data = parquet.write_parquet(morsel) with open("out.parquet", "wb") as f: f.write(data)
| Query shape | Rugo | PyArrow |
|---|---|---|
| SELECT * | ~26 ms | ~17 ms |
| SELECT 2 cols | ~9 ms | ~7 ms |
| WHERE P90 (~10% pass) | ~13 ms | ~27 ms |
| WHERE P99 (~1% pass) | ~10 ms | ~23 ms |
On narrow files PyArrow is faster across the board — the crossover is driven by how many columns can be skipped and how many rows are eliminated before the typed column build.
from rugo import csv # Schema without reading all data meta = csv.read_metadata("data.csv") print(meta.num_rows, [c["name"] for c in meta.schema_columns]) # Projected + filtered read with csv.read_csv( "data.csv", columns=["name", "score"], predicates=[("age", ">", 30)], ) as reader: for morsel in reader: print(morsel.column("name").to_pylist()) # TSV variant with csv.read_csv("data.tsv", delimiter="\t") as reader: for morsel in reader: ... # Write csv_bytes = csv.write_csv(morsel)
| Query shape | Rugo | PyArrow |
|---|---|---|
| SELECT * | ~1.09 s | ~1.38 s |
| SELECT 1 col | ~215 ms | ~1.4 s |
| WHERE ~10% pass | ~146 ms | ~1.84 s |
| WHERE ~1% pass | ~145 ms | ~1.83 s |
Advantage grows with selectivity — PyArrow reads and decodes every column on every query, regardless of projection or filter. On a wide, real-world row shape, even SELECT * favors Rugo.
from rugo import jsonl # Schema without reading all data meta = jsonl.read_metadata("events.jsonl") print(meta.num_rows, [c["name"] for c in meta.schema_columns]) # Projected + filtered read with jsonl.read_jsonl( "events.jsonl", columns=["id", "status"], predicates=[("status", "==", "active")], ) as reader: for morsel in reader: print(len(morsel), "rows matched") # Write a Morsel to JSONL bytes jsonl_bytes = jsonl.write_jsonl(morsel)
Rugo speaks Draken — a bundled columnar substrate that ships inside the wheel. There are two objects you will encounter.
A single typed column (similar to an Arrow Array).
to_pylist() → list · materialise as a Python listis_null() → Vector[bool] · null mask, one bool per rownull_bitmap() → bytes · raw 1-bit-per-row null bitmapin_list(values) → Vector[bool] · membership testbetween(lower, upper) → Vector[bool] · inclusive range testsum() / min() / max() → scalar · typed reductions; null-awaretake(indices) → Vector · gather rows by index listto_arrow() → pyarrow.Array · interop; requires PyArrow at runtimetype property · Draken type tag (e.g. DRAKEN_VARCHAR)len(vec) → int · row countvec = morsel.column("name") values = vec.to_pylist() # ["Mercury", "Venus", …]
A batch of rows across columns (similar to an Arrow RecordBatch).
column(name: str) → Vector · return a single column by namecolumn_names property → list[bytes] · ordered column namescolumn_types property → list · Draken type per columnschema property → list[dict] · name + type per columnselect(col_names: list) → Morsel · project to a subset of columnsrename(new_names: list) → Morsel · rename columns in orderfilter_mask(mask: Vector[bool]) → Morsel · keep rows where mask is truetake(indices) → Morsel · gather rows by index listslice(offset, length) → Morsel · contiguous row windowcombine(morsels) classmethod → Morsel · concatenate a list of Morselsfrom_vectors(col_names, col_vecs) classmethod → Morsel · construct from column name/vector pairsto_arrow() → pyarrow.Table · interop; requires PyArrow at runtimelen(morsel) → int · row countnum_columns property → int · column countnbytes property → int · approximate memory footprint in bytesfor morsel in reader: print(len(morsel), "rows") vec = morsel.column("id")
A read → write round-trip works naturally:
read_parquet yields Morsels,
write_parquet / write_csv /
write_jsonl all consume a Morsel.
from rugo import parquet from rugo.csv import write_csv from rugo.jsonl import write_jsonl with parquet.read_parquet("planets.parquet") as reader: for morsel in reader: csv_bytes = write_csv(morsel) jsonl_bytes = write_jsonl(morsel) pq_bytes = parquet.write_parquet(morsel)
Rugo isn't designed to decode data faster.
It's designed to avoid decoding data you don't
need.
Rugo was born out of Opteryx's need for a fast, low-memory Parquet reader — thin by design, and opinionated about what not to load.
Complete API reference, all options, limitations, and design notes.
A complete workflow on a real dataset: schema inspection, filtered streaming, aggregation, and format conversion.
Rugo is developed inside opteryx-core.
Source, examples, and issue tracking are all there.
Pre-built wheels for Linux x86-64/aarch64, macOS arm64. Python 3.11+.