Guide
Getting Started With Python Excel AutomationDeep dive

pandas vs Polars for Excel Workflows

Both read Excel through calamine and write through xlsxwriter, so the real difference is the transform. Compare the APIs, the speed, and how to move between them.

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.

Where the two libraries actually differ in an Excel pipeline Reading and writing are shared ground because both libraries delegate to calamine and xlsxwriter. The transform in the middle is where Polars and pandas genuinely differ. one pipeline, three stages read both use calamine transform the real difference write both use xlsxwriter the edges are shared; the middle is the choice

Prerequisites

Bash
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.

Python
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.

Two ways of expressing the same transform pandas builds the result by reassigning frames step by step, while Polars describes one expression graph that it optimises and runs across cores. pandas assign, then reassign one frame per step single-threaded by default forgiving of messy types Polars one expression graph no intermediates parallel across cores strict about types strictness is the feature, not the friction
Python
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.

Python
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.

Python
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.

Python
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.

Python
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

SymptomCauseFix
ModuleNotFoundError: fastexcelPolars' Excel reader is an optional extrapip install fastexcel, or pass engine="openpyxl" to read_excel
Polars read returns everything as Stringinfer_schema_length too small, or a header row of mixed typesPass infer_schema_length=None to scan the whole column, or a schema_overrides dict
write_excel raises on an existing fileIt builds a new workbook; it cannot open oneWrite a new file, or use openpyxl if the target must be edited in place
A conversion to pandas is slower than the transformObject-dtype strings are copied element by elementPass use_pyarrow_extension_array=True to to_pandas()
Dates arrive as integersExcel serial numbers were read without date inferenceCast explicitly, as in Fix Excel Serial Numbers Showing Instead of Dates

Performance and scale

Relative time on a one-million-row group-and-sort Reading is nearly identical when both libraries use calamine. The grouping and sorting step is several times faster in Polars, and a lazy scan over a Parquet copy is faster again. pandas transform baseline Polars transform multi-threaded Polars lazy scan columns pruned shared calamine read identical either way relative cost the read is shared ground; the transform is not

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:

Python
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.