pandas, Polars, Arrow, and DuckDB
pandas, Polars, Apache Arrow, and DuckDB overlap, but they are not four interchangeable brands of the same abstraction.
- pandas provides an eager, mutable-feeling Python DataFrame API with a broad analysis ecosystem.
- Polars provides expression-oriented eager and lazy DataFrames backed by its query engines.
- Arrow specifies columnar data structures and interchange/IPC interfaces; PyArrow also supplies compute and I/O libraries.
- DuckDB is an embedded analytical SQL database and execution engine with file and Arrow integration.
Choose a boundary by the operations, data size, ecosystem, language, deployment, and operational constraints—not a universal speed ranking.
Prerequisites
Complete Apache Arrow Memory Model, DuckDB for Local Analytics, and Polars Lazy DataFrames.
Learning goals
- Name the primary abstraction each tool provides.
- Implement one result with explicit schemas in all four tools.
- Identify where interchange may share buffers and where conversion can occur.
- Design a representative benchmark instead of generalizing from a toy input.
Worked example
Each path computes the same customer-level total. The final normalization exists only to compare observable results; it does not claim identical internal execution.
from pathlib import Path
import duckdb
import pandas as pd
import polars as pl
import pyarrow as pa
import pyarrow.csv as pacsv
source = Path("fixtures/data-engineering/files-tools/orders.csv")
expected = [(1, 100.0), (2, 25.0), (3, 80.0)]
pandas_frame = pd.read_csv(
source,
dtype={"order_id": "int64", "customer_id": "int64", "amount": "Float64", "status": "string"},
parse_dates=["ordered_at"],
)
pandas_result = list(
pandas_frame.groupby("customer_id", sort=True, dropna=False)["amount"].sum().items()
)
polars_result = [
tuple(row)
for row in (
pl.scan_csv(source, schema={
"order_id": pl.Int64, "ordered_at": pl.Date,
"customer_id": pl.Int64, "amount": pl.Float64, "status": pl.String,
})
.group_by("customer_id")
.agg(pl.col("amount").sum())
.sort("customer_id")
.collect()
.rows()
)
]
arrow_table = pacsv.read_csv(
source,
convert_options=pacsv.ConvertOptions(column_types={
"order_id": pa.int64(), "ordered_at": pa.date32(),
"customer_id": pa.int64(), "amount": pa.float64(), "status": pa.string(),
}),
)
arrow_result = [
tuple(row.values())
for row in arrow_table.group_by("customer_id").aggregate([("amount", "sum")])
.sort_by("customer_id").to_pylist()
]
duckdb_result = duckdb.sql("""
SELECT customer_id, CAST(SUM(amount) AS DOUBLE)
FROM read_csv(
'fixtures/data-engineering/files-tools/orders.csv', header=true,
columns={'order_id':'BIGINT','ordered_at':'DATE','customer_id':'BIGINT','amount':'DOUBLE','status':'VARCHAR'}
)
GROUP BY customer_id ORDER BY customer_id
""").fetchall()
assert pandas_result == polars_result == arrow_result == duckdb_result == expected
print(expected)
The assertion verifies semantic agreement on this fixture. It is intentionally not timed. A useful benchmark would preserve identical types and null semantics, include warm and cold reads where relevant, use representative scale and selectivity, cap threads consistently, measure peak memory and total I/O, repeat runs, and report hardware and versions.
Interchange boundaries
Arrow-aware APIs can sometimes expose existing buffers to another library. Whether that path is zero-copy depends on the exact source type, target type, chunking, null representation, mutability, ownership, and API version. Converting an Arrow string column to a consumer's incompatible string representation, combining chunks, or materializing Python objects can copy.
Keep transformations in one engine when that makes the plan simpler, and cross a boundary intentionally when another ecosystem provides needed functionality. Verify schema, nulls, ordering, categorical metadata, timestamp timezone, decimal precision, and buffer ownership at every boundary.
Limitations and common mistakes
- Syntax similarity does not guarantee identical null, grouping, ordering, or type behavior.
- “Uses Arrow” does not guarantee that every conversion shares buffers.
- A fastest result on one laptop, dataset, query, or version is not a universal ranking.
- pandas ecosystem compatibility, Polars lazy plans, Arrow interoperability, and DuckDB SQL each solve different constraints.
- Data larger than memory still requires a supported streaming, pushdown, batching, or out-of-core path; a library name alone is not a memory plan.
Practice
Design a benchmark for a 50-column Parquet dataset where a query selects three columns and one percent of rows. Specify correctness checks, cache policy, thread limits, repetitions, memory measurement, and which result would change your tool choice.
Next steps
Use Choosing a Data Processing Tool to turn these distinctions into a documented project decision.
Primary sources
Checked 2026-10-15: pandas scaling guidance, Polars pandas migration guide, Polars Arrow producer/consumer guide, Arrow C Data Interface, and DuckDB Python data ingestion.