Write a Polars DataFrame to Excel with Formatting
Polars can write the report as well as compute it. DataFrame.write_excel() drives xlsxwriter beneath a much shorter API, so a formatted workbook — number formats, an Excel table, frozen headers, autofitted columns — comes out of a single call rather than thirty lines of cell styling. This guide covers the arguments worth knowing, how to put several sheets in one file, and the point at which you should drop through to xlsxwriter itself. It is the writing half of Reading Excel with Polars and Arrow.
Prerequisites
pip install polars xlsxwriter
write_excel raises immediately if xlsxwriter is missing, since it is the engine doing the work.
A formatted sheet in one call
import polars as pl
df = pl.DataFrame({
"region": ["North", "South", "East", "West"],
"orders": [412, 388, 195, 264],
"revenue": [128_400.50, 96_220.00, 51_130.25, 74_905.75],
"margin": [0.184, 0.212, 0.147, 0.196],
})
df.write_excel(
"regional_summary.xlsx",
worksheet="Summary",
table_style="Table Style Medium 9",
autofit=True,
freeze_panes="A2",
column_formats={
"revenue": "#,##0.00",
"margin": "0.0%",
"orders": "#,##0",
},
)
That produces a workbook a stakeholder can use immediately: a banded Excel table with filter buttons, thousands separators on money, percentages rendered as percentages, columns wide enough to read, and a header row that stays visible while scrolling. The format strings are Excel's own — the same vocabulary described in Applying Number and Date Formats in Excel.
Add a totals row and column widths
Two arguments handle the details reviewers always ask for:
df.write_excel(
"regional_summary.xlsx",
worksheet="Summary",
table_style="Table Style Medium 2",
column_totals={"orders": "sum", "revenue": "sum", "margin": "average"},
column_widths={"region": 140, "revenue": 120},
header_format={"bold": True, "bg_color": "#DDEBF7", "border": 1},
float_precision=2,
)
column_totals writes a real Excel totals row — the values are SUBTOTAL formulas, so they recalculate when a reader filters the table rather than sitting there as stale numbers. column_widths takes precedence over autofit for the columns it names, which is what you want when a header is far longer than its values.
Highlight values with a conditional format
conditional_formats accepts the xlsxwriter dictionaries directly, so anything that library can express is available:
df.write_excel(
"regional_summary.xlsx",
worksheet="Summary",
table_style="Table Style Medium 9",
autofit=True,
conditional_formats={
"margin": {
"type": "cell",
"criteria": "<",
"value": 0.15,
"format": {"bg_color": "#FFC7CE", "font_color": "#9C0006"},
},
"revenue": {"type": "data_bar", "bar_color": "#5B5CF0"},
},
)
A data bar turns a column of numbers into an at-a-glance ranking without a chart, and a cell rule flags the rows that need attention. The equivalents in openpyxl and xlsxwriter are covered in Applying Conditional Formatting with openpyxl and Apply conditional formatting with xlsxwriter.
Put several sheets in one workbook
write_excel writes one worksheet per call, so a multi-sheet report means owning the workbook object yourself:
import polars as pl
import xlsxwriter
summary = pl.DataFrame({"region": ["North", "South"], "revenue": [128_400.5, 96_220.0]})
detail = pl.DataFrame({"order_id": ["00417", "00418"], "revenue": [1_204.0, 980.5]})
with xlsxwriter.Workbook("report.xlsx") as wb:
summary.write_excel(workbook=wb, worksheet="Summary", autofit=True,
table_style="Table Style Medium 9",
column_formats={"revenue": "#,##0.00"})
detail.write_excel(workbook=wb, worksheet="Detail", autofit=True,
table_style="Table Style Light 1")
Using the context manager matters: xlsxwriter only writes the file on close(), and a workbook that is never closed produces a zero-byte or corrupt .xlsx. The multi-sheet report pattern in full is Write multiple DataFrames to one Excel file.
Drop through to xlsxwriter for anything else
Polars deliberately does not wrap every xlsxwriter feature. When you need a chart, a merged title, or a sheet-level setting, take the objects back and use the library directly:
import polars as pl
import xlsxwriter
df = pl.DataFrame({"region": ["North", "South", "East"], "revenue": [128400.5, 96220.0, 51130.25]})
with xlsxwriter.Workbook("with_chart.xlsx") as wb:
ws = df.write_excel(workbook=wb, worksheet="Summary", autofit=True,
table_style="Table Style Medium 9")
chart = wb.add_chart({"type": "column"})
chart.add_series({
"name": "Revenue",
"categories": ["Summary", 1, 0, df.height, 0],
"values": ["Summary", 1, 1, df.height, 1],
"fill": {"color": "#5B5CF0"},
})
chart.set_title({"name": "Revenue by region"})
chart.set_legend({"none": True})
ws.insert_chart("E2", chart, {"x_scale": 1.2, "y_scale": 1.2})
write_excel returns the worksheet, which is the hand-off point. From there the whole xlsxwriter API is available — see Add a chart to an Excel file with xlsxwriter.
Format by dtype instead of by name
Naming every column in column_formats does not survive a schema change. Deriving the formats from the frame's own types does, which matters for a report whose columns come from a query rather than a fixed list:
import polars as pl
def formats_for(df: pl.DataFrame) -> dict[str, str]:
"""Money-like floats get separators, dates get ISO, integers get grouping."""
out: dict[str, str] = {}
for name, dtype in df.schema.items():
if dtype in (pl.Float32, pl.Float64):
out[name] = "0.0%" if name.endswith(("rate", "margin", "share")) else "#,##0.00"
elif dtype in (pl.Int8, pl.Int16, pl.Int32, pl.Int64):
out[name] = "#,##0"
elif dtype == pl.Date:
out[name] = "yyyy-mm-dd"
elif dtype == pl.Datetime:
out[name] = "yyyy-mm-dd hh:mm"
return out
df.write_excel("summary.xlsx", autofit=True, column_formats=formats_for(df))
A naming convention does the rest of the work: any column ending in rate, margin or share is a proportion and gets a percentage format, everything else numeric gets thousands separators. Add a column next month and it is formatted correctly with no change to the report code.
Ship the same workbook every time
A generated report is easier to trust when successive runs differ only in their numbers. Three habits get you there:
- Sort deterministically before writing, so row order does not wander between runs.
- Round in the data, not only in the format, when a figure is quoted elsewhere —
pl.col("revenue").round(2)— so a reader copying a cell gets the number they see. - Stamp the run into a cell or the sheet name, so two copies of the file on someone's desktop can be told apart.
report = summary.sort("region")
report.write_excel(
f"regional_summary_{run_date:%Y-%m-%d}.xlsx",
worksheet=f"Summary {run_date:%b %Y}",
autofit=True,
table_style="Table Style Medium 9",
)
Dated filenames also stop a scheduled job silently overwriting last month's output — the failure mode discussed in Automating Reporting Workflows.
Common pitfalls and gotchas
- Forgetting to close the workbook. Use
with xlsxwriter.Workbook(...)or callclose()explicitly; nothing is written until then. - Expecting to edit a template. xlsxwriter creates new files only. Template filling belongs to openpyxl — Fill an Excel template with Python and openpyxl.
- Duplicate table names. Two sheets whose tables share a name make Excel offer to repair the file. Give each
worksheeta distinct name and let Polars derive the table name from it. - Sheet names over 31 characters or containing
/ \ ? * [ ]are rejected by Excel — sanitise names derived from data. - Percentages written as text. Store
0.184and format it as0.0%; writing the string"18.4%"gives you text that cannot be totalled.
Performance and scale notes
xlsxwriter builds the whole workbook in memory before writing, so a very large sheet can be expensive. For hundreds of thousands of rows, write through xlsxwriter's constant_memory mode — pass it when constructing the workbook and hand that workbook to write_excel — which streams rows to disk at the cost of forbidding some features, including autofit and any write that revisits an earlier row. The details and limits are in Write a million rows to Excel with xlsxwriter constant memory. A better answer for genuinely large output is usually not to send it to Excel at all: write the detail to Parquet or CSV and put only the summary in the workbook.
Conclusion
write_excel() covers the presentation layer of a report in one call — table styling, number formats, autofit, frozen panes, totals and conditional formats — and hands you the underlying xlsxwriter objects the moment you need more. Compose several calls against one workbook for a multi-sheet report, keep openpyxl for templates and in-place edits, and store numbers as numbers so Excel's own formatting can do its job.
Frequently asked questions
What does Polars use to write xlsx?
xlsxwriter. write_excel is a high-level wrapper over it, which is why xlsxwriter must be installed and why its formatting vocabulary — number format strings, table styles, conditional format dictionaries — appears in the arguments.
Can I write several DataFrames into one workbook?
Yes. Create an xlsxwriter Workbook yourself and pass it to write_excel once per sheet, then close it. Each call adds a worksheet to the same file.
Can write_excel edit an existing workbook or fill a template? No. xlsxwriter only creates new files, so template filling and in-place edits still belong to openpyxl.
How do I add a chart?
Get the underlying worksheet and workbook objects from write_excel, then use the xlsxwriter chart API directly. Polars hands you the objects rather than wrapping every xlsxwriter feature.
Does autofit measure the rendered text?
It estimates from the string lengths in each column, which is close enough for most reports. Set an explicit column_widths for columns whose headers are much longer than their values.
Related
- Up: Reading Excel with Polars and Arrow — the read side, and where this output fits in a pipeline.
- Read an Excel file with polars.read_excel — completing the round trip.
- Write a formatted Excel report with xlsxwriter — the same output built by hand, for when you need every option.
- Write multiple DataFrames to one Excel file — the multi-sheet report, pandas-side.
- Apply a reusable style theme across an Excel report — keeping several generated reports visually consistent.