pandas vs Polars vs PySpark: which to use for your data size
pandas works in memory on one core, Polars runs lazy queries on every core and, since Polars 2.0, streams them in batches by default, and PySpark spreads work over a cluster. How to choose by the size of your data and where it lives, the same job written in all 3, and what changed in pandas 3 and Polars 2.
Tecyfy · Published · 8 min read

Use pandas when your data fits comfortably in memory, Polars when you want one machine to go faster or handle more than its memory, and PySpark when the data is too big for one machine or already lives on a Spark platform. The deciding number is the size of the table in memory compared with the RAM you have, not the number of rows.
All 3 are DataFrame libraries for Python, and since this autumn 2 of them have changed in ways worth knowing: pandas 3.0 changed how text and copies work, and Polars 2.0 made streaming the default.
How they differ
| pandas 3.1 | Polars 2.0 | PySpark 4.2 | |
|---|---|---|---|
| Runs on | One machine, mostly one core | One machine, every core | A cluster (or local cores) |
| Data must fit in memory | Yes, with room for copies | No, with the streaming engine | No, spread over executors |
| Execution | Eager: each line runs | Lazy query, optimised, then run | Lazy plan, optimised, then run |
| Memory format | NumPy; PyArrow for text if installed | Apache Arrow | Spark's own, across executors |
| Row index | Yes | No | No |
| Best at | Exploration, small data, the Python ecosystem | Fast single-machine pipelines | Terabytes, lakehouses, streaming |
pandas' own guide says it plainly: pandas works in memory, and even a dataset that is a sizable fraction of memory becomes unwieldy, because some operations make intermediate copies. Polars and Spark both build a plan first, so they can read only the columns and rows a query needs (projection and predicate pushdown) before doing any work.
The same job in each
Revenue by country from paid orders, read from Parquet:
import pandas as pd
orders = pd.read_parquet("orders.parquet", columns=["country", "amount", "status"])
result = (
orders[orders["status"] == "paid"]
.groupby("country", as_index=False)["amount"].sum()
.sort_values("amount", ascending=False)
)
import polars as pl
result = (
pl.scan_parquet("orders.parquet")
.filter(pl.col("status") == "paid")
.group_by("country")
.agg(pl.col("amount").sum())
.sort("amount", descending=True)
.collect()
)
from pyspark.sql import SparkSession, functions as F
spark = SparkSession.builder.getOrCreate()
result = (
spark.read.parquet("s3://my-bucket/orders/")
.filter(F.col("status") == "paid")
.groupBy("country")
.agg(F.sum("amount").alias("amount"))
.orderBy(F.desc("amount"))
)
result.show()
In pandas you choose the columns yourself; Polars' scan_parquet and Spark's reader work them out from the query. The code is close enough that moving between the 3 is mostly renaming.
A worked sizing
An orders table has 6 number and date columns and 3 text columns averaging 12 characters. In memory each number takes 8 bytes, and each text value its bytes plus up to 16 bytes of bookkeeping, so a row is about 48 + 3 × 28 = 132 bytes.
- 50 million rows: about 6.1 GB. On a 32 GB laptop that is under a fifth of memory: pandas works, and Polars will be quicker on many cores.
- 400 million rows: about 49 GB, more than the 32 GB machine holds. pandas cannot load it at once. Polars' lazy API with the streaming engine reads it in batches on the same laptop.
- 50 billion rows: about 6 TB. One machine streaming it would take hours just to read it; this is Spark's job. The Spark cluster calculator sizes the cluster.
Parquet on disk is usually much smaller, because it compresses, so judge by the in-memory size.
Bar = machine memory; dashed line = a third of it.
Use: pandas or Polars Fits
The table takes about 6.1 GB in memory, 19% of 32 GB. pandas or Polars: It fits in memory with room for the copies a pipeline makes. Polars will use every core; pandas is fine if your code already uses it.
Arrow sizes: 8 bytes for each 64-bit number or date, and each text value's bytes plus 16; an estimate, null masks left out. Parquet on disk is usually smaller, because it compresses. The thirds and the 2 TB line are our rule of thumb, not a library limit.
What changed this year
pandas 3.0 (January 2026; 3.1 came out on 22 October): text columns are now a dedicated str type, backed by PyArrow when installed, so code that checks for object dtype needs updating. Copy-on-write is the only behaviour: every result of indexing acts as a copy, so chained assignment no longer works and the SettingWithCopyWarning is gone. pd.col("a") + pd.col("b") replaces many lambdas. It needs Python 3.11 or newer.
Polars 2.0 (6 October 2026): LazyFrame.collect() now uses the streaming engine by default, which also handles data larger than memory. The catch: streaming does not keep row order for group_by, joins or unpivot. If a pipeline relied on rows coming out in the order they went in, add an explicit sort (as the example above does) or maintain_order=True where the operation offers it.
Which fits
- pandas if the data is small, you are exploring in a notebook, or a library you depend on wants a pandas DataFrame.
- Polars for single-machine pipelines, larger-than-memory files on one VM, and anywhere a Spark cluster's start-up and cost are not worth it.
- PySpark for data spread over many files in a lake, tables in Delta Lake or Iceberg, streaming, and teams already on Databricks. If Spark jobs run out of memory, our guide to Spark out of memory errors helps.
What to do
- Estimate the in-memory size with the widget above, not the row count.
- Below a third of RAM, stay with what your team knows.
- Above that, try Polars' lazy API before reaching for a cluster.
- Upgrading to Polars 2.0? Search for code that depends on row order. Upgrading to pandas 3? Search for
objectdtype checks. - To learn the Spark side properly, start with the PySpark DataFrames module of our data engineering course.
Questions
- Should I use pandas, Polars or PySpark?
- Use pandas when the data fits comfortably in memory and your code or libraries already use it. Use Polars when you want speed on one machine or the data is larger than memory: it uses every core and its streaming engine works in batches. Use PySpark when the data is too big for one machine to read in reasonable time, or already lives in a Spark platform such as Databricks.
- Is Polars faster than pandas?
- Usually, for the same work on one machine. Polars runs many operations in parallel across cores and optimises lazy queries before running them, while the core of pandas is single-threaded and runs each step as it is written. Measure on your own data; small tables finish quickly in either.
- What changed in Polars 2.0?
- Released on 6 October 2026, Polars 2.0 makes lazy queries run on the streaming engine by default: collect() with engine auto now means streaming. The streaming engine does not guarantee row order for operations such as group_by, joins and unpivot, so code that relied on incidental order must sort explicitly or pass maintain_order.
- What changed in pandas 3.0?
- pandas 3.0, released on 21 January 2026, infers text columns as a dedicated str data type (backed by PyArrow when it is installed), makes copy-on-write the only behaviour so every indexing result acts as a copy, adds pd.col() expressions, and needs Python 3.11 or newer.
- When is PySpark overkill?
- When the data fits on one machine. A Spark cluster adds start-up time, a driver and executors to size, and cluster cost, so a few gigabytes are usually faster and cheaper in Polars or pandas on a laptop or a single VM.
Sources