Anti-patterns¶
These patterns are common, legal, and expensive. Avoid them when throughput or memory matters.
Iterating a table source row-by-row¶
This works, but it reconstructs Python dicts from the Arrow table. If the source is table-shaped and you need row access, consider using to_arrow() and pyarrow vectorized operations instead.
Chaining Python callables¶
result = (
source
| (lambda table: transform(table))
| (lambda table: another_transform(table))
).to_arrow()
Each callable materializes a full Python object (usually a pyarrow.Table or list of dicts) and breaks fusion. Prefer fused stages or move the logic into Rust.
Repeated to_pandas / to_arrow¶
Each call re-runs the pipeline. Cache the table once and reuse it:
Ignoring plan_overrides¶
class MySource(Source):
def _read_arrow(self, *, plan_overrides=None, **kwargs):
return my_rust_read(self.path, **kwargs) # plan_overrides lost!
If an adapter ignores plan_overrides, fused stages silently fall back to Python execution. Always forward plan_overrides to the Rust reader.
Wrong engine choice¶
For small files, columnar mode is usually fastest. For huge files, stream mode keeps memory flat. Parallel mode only wins for large, CPU-bound, cached files.
Using auto_dict in parallel mode for throughput¶
auto_dict forces the merge path in parallel mode. If throughput is the goal, use explicit dictionary_columns for only the columns that need it, or switch to columnar mode.
Not declaring types for numeric filters¶
Without field_types={"amount": "float64"}, the engine may store amount as a string and skip the vectorized compare filter. Declare the type so the filter runs in Arrow.
Summary¶
- Cache tables; do not re-run pipelines.
- Forward
plan_overridesin adapters. - Keep Python callables out of the hot path.
- Match the engine mode to the file size and workload.
- Declare types for numeric filters.