Polars
The fast DataFrame library for Rust - lazy query optimization, vectorized execution, and Arrow-backed memory layout.
Recipe
Quick-reference recipe card - copy-paste ready.
use polars::prelude::*;
fn top_spenders(path: &str) -> PolarsResult<DataFrame> {
LazyCsvReader::new(path.into())
.with_has_header(true)
.finish()?
.group_by([col("customer_id")])
.agg([col("amount").sum().alias("total")])
.sort(["total"], SortMultipleOptions::default())
.limit(10)
.collect()
}When to reach for this:
- Replacing pandas scripts that bottleneck on single-threaded Python loops
- ETL where you need lazy predicate pushdown on CSV or Parquet
- In-process analytics without standing up Spark or DuckDB as a service
- Rust services that already use Arrow and need a DataFrame API
Working Example
use polars::prelude::*;
fn main() -> PolarsResult<()> {
let df = df! {
"region" => ["west", "east", "west", "north"],
"amount" => [120i64, 80, 200, 50],
}?;
let summary = df
.lazy()
.filter(col("amount").gt(lit(60)))
.group_by([col("region")])
.agg([
col("amount").sum().alias("total"),
col("amount").mean().alias("avg"),
])
.sort(["total"], SortMultipleOptions::default())
.collect()?;
println!("{}", summary);
Ok(())
}What this demonstrates:
- Building a small frame in memory for tests and prototypes
- Switching to lazy mode with
.lazy()for optimized execution - Group-by aggregations with multiple expressions in one plan
Deep Dive
How It Works
- Polars stores columns in Arrow-compatible buffers for cache-friendly scans.
- Lazy API builds a logical plan; the optimizer reorders filters and projections.
collect()executes the plan - that is your memory and CPU spike boundary.- Eager
DataFramemethods run immediately - simpler mental model, fewer optimizations.
Lazy vs Eager
| Mode | API | Best for |
|---|---|---|
| Eager | DataFrame::filter, group_by | Unit tests, tiny tables |
| Lazy | LazyFrame + collect() | Large files, multi-step pipelines |
Rust Notes
// Prefer explicit aliases in agg - unnamed exprs are hard to spot in explain plans.
.col("amount").sum().alias("total")
// Use .explain() on lazy plans during development.
println!("{}", lf.explain(true)?);Gotchas
- Collecting too early - calling
collect()after every step defeats lazy optimization. Fix: chain transforms, collect once at the end. - String-typed numeric columns - CSV inference defaults to Utf8. Fix: pass
with_dtypesor cast after load. - Join key dtype mismatch -
i32vsi64keys silently drop rows. Fix:castboth sides to the same type before join. - Assuming pandas index semantics - Polars has no hidden index; joins use explicit keys. Fix: add an
idcolumn when you need stable row identity. - OOM on wide Parquet -
select *on 500 columns loads everything. Fix: project only needed columns in the lazy scan.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| DataFusion SQL | Team thinks in SQL over registered tables | You want a Rust expression DSL in application code |
| pandas (Python) | Notebook exploration with a huge ecosystem | Production ETL needing single-binary deploy |
| DuckDB embedded | SQL-first analytics on files | You are standardizing on pure Rust dependencies |
| ndarray + manual loops | Tiny numeric kernels | Relational transforms and I/O heavy pipelines |
FAQs
Should I default to lazy or eager?
Default to lazy for file-backed pipelines. Use eager for tests, REPL-style exploration, and frames that already fit in memory.
How do I debug a slow plan?
Call .explain(true) on the LazyFrame before collect() to see predicate pushdown and projection pruning.
Does Polars use multiple threads?
Yes. Many operations parallelize across columns and row groups. Control thread count with POLARS_MAX_THREADS when colocating services.
Can Polars read cloud object storage?
Use scan_parquet / scan_csv with compatible paths and feature flags for cloud backends, or stage files locally for simpler ops.
How do I handle nulls?
Use fill_null, drop_nulls, or coalesce expressions. Nullable dtypes must match schema expectations on write.
What changed in Polars 0.46+?
Stay on the expression API (col, lit, when) documented for 0.46+. Older lazy::dsl patterns may differ - pin versions in CI.
How do I export to Python?
Convert to Arrow RecordBatch and hand off via PyArrow - see Interop with Python/pandas.
Is Polars good for streaming?
For bounded memory, combine chunked CSV reads or scan Parquet row groups - see Streaming & Large Data.
How do I persist results?
Use ParquetWriter or CsvWriter on eager frames after the final collect().
Can I mix Rust and SQL?
Register Polars output as Arrow tables in DataFusion for SQL layers - see DataFusion.
Related
- Data Basics - section orientation
- Apache Arrow - columnar memory under Polars
- CSV/Parquet I/O - I/O tuning
- Streaming & Large Data - chunked processing
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.