pandas vs Polars for Excel Workflows
Polars arrived as a faster, stricter alternative to pandas, and for the middle of an Excel pipeline — the joins, groupings and reshaping between reading a workbook and writing one — it usually deserves the reputation. At the two edges the picture is more nuanced: both libraries delegate the actual spreadsheet parsing to the same handful of engines, so "which is faster at Excel" is often a question about calamine rather than about either library. This guide, part of Choosing a Python Excel Library, compares them where the difference is real.
Prerequisites
pip install polars pandas fastexcel xlsxwriter openpyxl
fastexcel is the binding Polars uses for Excel reads (it wraps calamine); xlsxwriter is what
write_excel builds on. Both are optional extras, and read_excel/write_excel raise a clear
import error if they are missing.
Reading the same workbook with each
The calls are close to identical, and so is the time they take when both use the same parser.
import pandas as pd
import polars as pl
pdf = pd.read_excel("sales.xlsx", sheet_name="Raw", engine="calamine")
pdf_types = pdf.dtypes
pldf = pl.read_excel("sales.xlsx", sheet_name="Raw")
print(pldf.schema)
print(pdf_types)
The differences show in the schema, not the clock. Polars has a real String type rather than
object, distinguishes Int64 from Float64 without silently promoting on a missing value, and
has no index — so nothing is quietly carried along that you did not ask for. On an Excel file that
last point matters more than it sounds: index columns are the origin of the mysterious unnamed
first column in half the spreadsheets produced by Python.
The transform: expressions versus method chains
This is where the two libraries genuinely diverge. pandas mutates and reassigns; Polars describes a computation and executes it. The Polars version is longer to read the first time and considerably harder to get subtly wrong.
import polars as pl
summary = (
pl.read_excel("sales.xlsx", sheet_name="Raw")
.filter(pl.col("Revenue").is_not_null())
.with_columns(
(pl.col("Revenue") * 0.2).alias("Tax"),
pl.col("Region").str.strip_chars().str.to_uppercase(),
)
.group_by("Region")
.agg(
pl.col("Revenue").sum().alias("Revenue"),
pl.col("Revenue").mean().round(2).alias("Avg deal"),
pl.len().alias("Deals"),
)
.sort("Revenue", descending=True)
)
print(summary)
The same pipeline in pandas needs a dropna, an assignment per derived column, a groupby().agg()
with a dictionary, and a sort_values — and every step produces an intermediate frame. Polars keeps
it as one expression graph, which is why it parallelises across cores without being asked.
Writing a formatted workbook from Polars
write_excel is the part that surprises people coming from pandas: it is a formatting API, not just
a dump. Table styles, per-column number formats, conditional formats and column autofit are all
keyword arguments, and underneath it is the same xlsxwriter used by
Building Excel Reports with XlsxWriter.
summary.write_excel(
"regional.xlsx",
worksheet="By region",
table_style="Table Style Medium 9",
column_formats={"Revenue": "#,##0.00", "Avg deal": "#,##0.00"},
conditional_formats={"Revenue": "data_bar"},
autofit=True,
freeze_panes="A2",
)
Getting that far in pandas means an ExcelWriter, a reach through to writer.book, a format object
per column and a set_column call for each — perhaps fifteen lines to this one. If the deliverable
is a formatted sheet and the transform is already in Polars, there is no reason to hand back to
pandas to write it. Write a Polars DataFrame to Excel with Formatting
goes through the full set of options.
Moving between the two
Neither choice is permanent. Arrow sits underneath both, so a conversion is cheap and often zero-copy for numeric columns — which makes "Polars for the heavy transform, pandas for the one library that only speaks DataFrame" a perfectly reasonable design.
import polars as pl
pldf = pl.read_excel("sales.xlsx")
pdf = pldf.to_pandas(use_pyarrow_extension_array=True) # keeps strings on the Arrow side
back = pl.from_pandas(pdf)
print(back.equals(pldf))
The conversion is where the two type systems meet, and it is worth checking rather than assuming: a
pandas object column of mixed ints and strings becomes a Polars String on the way over, and does
not become mixed again on the way back.
Where pandas still wins
Three things keep pandas in Excel work regardless of speed. It has a far larger surface of
spreadsheet-shaped conveniences — read_excel(sheet_name=None) returning a dict of every tab,
pivot_table with margins, to_excel straight onto an open ExcelWriter. It is what every other
library expects: plotting, statistics, machine-learning and reporting packages take DataFrames, and
"DataFrame" means the pandas one in most of them. And its coercion helpers are more forgiving of
genuinely dirty data, which is the normal state of a spreadsheet that a person maintains.
import pandas as pd
# The messy-column workflow pandas is unusually good at.
frame = pd.read_excel("messy.xlsx")
frame["Amount"] = pd.to_numeric(frame["Amount"], errors="coerce")
frame["Date"] = pd.to_datetime(frame["Date"], errors="coerce", dayfirst=True)
report = frame[frame[["Amount", "Date"]].isna().any(axis=1)]
print(f"{len(report)} rows need attention")
Polars can do all of that, with strict=False casts and str.to_date, but it will make you say
what you mean at each step. That is the right trade once the data is clean and an obstacle while
you are still finding out what is in it — the workflow described in
Cleaning Excel Data with Pandas.
Multi-sheet workbooks in both libraries
Reporting workbooks rarely have one tab, and the two libraries handle that differently enough to matter. pandas returns a dictionary keyed by sheet name; Polars returns one too, but its reader takes a list of sheet names or indices directly and its frames carry no index to reconcile afterwards.
import pandas as pd
import polars as pl
pandas_tabs = pd.read_excel("book.xlsx", sheet_name=None) # {name: DataFrame}
combined_pd = pd.concat(
[frame.assign(Source=name) for name, frame in pandas_tabs.items()],
ignore_index=True,
)
polars_tabs = pl.read_excel("book.xlsx", sheet_name=["Jan", "Feb", "Mar"])
combined_pl = pl.concat(
[frame.with_columns(pl.lit(name).alias("Source")) for name, frame in polars_tabs.items()],
how="diagonal",
)
print(combined_pd.shape, combined_pl.shape)
how="diagonal" is the detail worth keeping: it unions tabs whose columns do not match exactly,
filling the gaps with nulls, where a plain pl.concat would raise. pandas' concat does the same
thing silently, which is friendlier until a renamed column in one month's tab produces two columns
where you expected one. The trade is the same one that runs through this whole comparison — the
stricter library tells you about the problem, and the forgiving one hands you a result that looks
fine. Concatenate Excel Sheets with Different Columns
covers the failure modes of that union in detail.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
ModuleNotFoundError: fastexcel | Polars' Excel reader is an optional extra | pip install fastexcel, or pass engine="openpyxl" to read_excel |
Polars read returns everything as String | infer_schema_length too small, or a header row of mixed types | Pass infer_schema_length=None to scan the whole column, or a schema_overrides dict |
write_excel raises on an existing file | It builds a new workbook; it cannot open one | Write a new file, or use openpyxl if the target must be edited in place |
| A conversion to pandas is slower than the transform | Object-dtype strings are copied element by element | Pass use_pyarrow_extension_array=True to to_pandas() |
| Dates arrive as integers | Excel serial numbers were read without date inference | Cast explicitly, as in Fix Excel Serial Numbers Showing Instead of Dates |
Performance and scale
The read is a wash when both libraries use calamine, because they are running the same Rust parser. The transform is where Polars pulls ahead, and the gap widens with row count: it is multi-threaded by default and never materialises the intermediate frames a pandas chain produces. Memory follows the same shape — Arrow buffers are more compact than NumPy object arrays for strings, which is the column type Excel exports produce most.
For very large inputs Polars has one more gear that pandas has no equivalent of. scan_parquet
builds a lazy query and only reads the columns and rows the query needs, so a pipeline that starts
by converting the workbook once can then run against a fraction of the data:
import polars as pl
pl.read_excel("huge.xlsx").write_parquet("huge.parquet") # once
result = (
pl.scan_parquet("huge.parquet")
.filter(pl.col("Region") == "North")
.group_by("Rep").agg(pl.col("Revenue").sum())
.collect()
)
Excel itself has no lazy mode, so this only pays off when the same workbook is queried repeatedly — the conversion argument made in Convert Excel Files to Parquet with Python.
Conclusion
Choose Polars for the middle of the pipeline and pandas for the edges of the ecosystem. Polars is faster and stricter on transforms, has a genuinely better formatted-Excel writer through xlsxwriter, and scales further before memory becomes the constraint. pandas remains the language every other library speaks and the more forgiving tool while data is still messy. Because Arrow sits under both, the decision is reversible in a single line, so it is worth making per pipeline rather than per team.
Frequently asked questions
Does Polars read Excel without pandas installed? Yes. polars.read_excel() calls python-calamine (or openpyxl, or xlsx2csv) directly and returns a Polars DataFrame. pandas is not involved and does not need to be installed.
Can I convert between the two without copying the data? Mostly. df.to_pandas() and pl.from_pandas() go through Arrow, which is zero-copy for numeric and boolean columns and a real copy for Python object strings. Passing use_pyarrow_extension_array=True keeps strings on the Arrow side too.
Does Polars write formatted Excel? Yes — write_excel wraps xlsxwriter, so it accepts table styles, column formats, conditional formats and autofit in one call. It cannot edit an existing workbook, because xlsxwriter cannot.
Is Polars always faster? For the transform, usually, and by more as the data grows. For the Excel read itself the parser decides, and both libraries can use calamine — so a like-for-like read is close. On a 2,000-row monthly report neither is measurably faster.
Which one handles messy spreadsheet data better? pandas, on balance. Its coercion helpers — to_numeric with errors='coerce', fillna, interpolate — are more forgiving of the sort of mixed-type column an Excel export produces. Polars is stricter, which is a virtue once the data is clean and a friction before that.
Related
- Up one level: Choosing a Python Excel Library — the full landscape including openpyxl, xlsxwriter and xlwings.
- Reading Excel with Polars and Arrow — the Polars-side topic, from reads to Parquet conversion.
- Read an Excel File with polars.read_excel — engine choice, schemas and multi-sheet reads in detail.
- openpyxl vs pandas for Excel Automation — the other half of the library question.
- Speed Up pandas Excel Reads with the calamine Engine — the parser both libraries can share.