How to Write a Pandas DataFrame to Excel in Python

Every quarter, a sales manager I work with asks for the same thing: the numbers from my pandas script, but as a workbook she can open, filter and forward. That’s the job of a pandas write DataFrame to Excel step.

Renaming a CSV to .xlsx doesn’t work; Excel warns that the format and extension don’t match.

Quick answer: call DataFrame.to_excel() with a file name. Add index=False so the row numbers don’t become an extra column:

df.to_excel("q1_sales.xlsx", sheet_name="Q1 2026", index=False)

In this tutorial, I’ll export one sheet, write several sheets with ExcelWriter, append to an existing file, format dates and USD values, add a chart and fix the errors you’ll actually hit. Everything below ran on Python 3.12.5, pandas 3.0.6, openpyxl 3.1.5 and XlsxWriter 3.3.1.

How do you write a pandas DataFrame to an Excel file?

You need pandas plus one Excel engine: openpyxl or XlsxWriter. Install both with pip install pandas openpyxl xlsxwriter. If the import still fails, my notes on a ModuleNotFoundError after pip install usually explain why.

The running sample is q1_sales.py: six orders from January to March 2026, with US regions, reps and revenue in USD. Every example imports it:

import pandas as pd

sales = pd.DataFrame({
    "Order Date": pd.to_datetime(["2026-01-06", "2026-01-19", "2026-02-03",
                                  "2026-02-24", "2026-03-10", "2026-03-27"]),
    "Region": ["Northeast", "Midwest", "South", "West", "Northeast", "West"],
    "Rep": ["Emily Carter", "James Wilson", "Olivia Brown",
            "Liam Davis", "Emily Carter", "Ava Miller"],
    "Units": [42, 18, 35, 27, 50, 31],
    "Revenue": [12590.50, 5418.00, 9875.25, 8120.40, 14995.00, 9310.75],
})

Now write it to a sheet named Q1 2026 and read it back to prove the file is complete:

import pandas as pd
from q1_sales import sales

sales.to_excel("q1_sales.xlsx", sheet_name="Q1 2026", index=False)

check = pd.read_excel("q1_sales.xlsx", sheet_name="Q1 2026")
print(check.shape)
print(check.to_string(index=False))
print("Total revenue:", f"${check['Revenue'].sum():,.2f}")

Output:

(6, 5)
Order Date    Region          Rep  Units  Revenue
2026-01-06 Northeast Emily Carter     42 12590.50
2026-01-19   Midwest James Wilson     18  5418.00
2026-02-03     South Olivia Brown     35  9875.25
2026-02-24      West   Liam Davis     27  8120.40
2026-03-10 Northeast Emily Carter     50 14995.00
2026-03-27      West   Ava Miller     31  9310.75
Total revenue: $60,309.90
Command Prompt output of pandas to_excel writing the Q1 2026 sales DataFrame and reading it back with read_excel
The file reads back as 6 rows and 5 columns, and the revenue still totals $60,309.90.

In Excel, the data is right but plain: no number formats, and the date column is too narrow for date plus time.

q1_sales.xlsx opened in Microsoft Excel after pandas to_excel, showing the Q1 2026 sheet with order dates, regions, reps, units and revenue
The workbook in Excel: one sheet named Q1 2026, dates showing as ######## and revenue without a currency format.

By default the file lands in the folder you ran the script from. Pass a full path, or a check that the file exists first if you don’t want to overwrite last quarter’s report.

Why does my Excel file have an extra index column?

to_excel() writes the DataFrame index as column A unless you say otherwise. Read that file back and pandas names it Unnamed: 0:

import pandas as pd
from q1_sales import sales

sales.to_excel("with_index.xlsx")                 # index=True is the default
print(pd.read_excel("with_index.xlsx").columns.tolist())
print(pd.read_excel("with_index.xlsx", index_col=0).columns.tolist())

Output:

['Unnamed: 0', 'Order Date', 'Region', 'Rep', 'Units', 'Revenue']
['Order Date', 'Region', 'Rep', 'Units', 'Revenue']

So use index=False when the index is only a row counter. Keep it when it carries meaning, like a date index, and read it back with index_col=0.

Already stuck with the column? Here’s how to drop the Unnamed column in pandas.

Write multiple DataFrames to separate Excel sheets with ExcelWriter

Calling to_excel() twice on the same file name replaces the whole file. To keep several sheets, open one pd.ExcelWriter and pass it to each call:

import pandas as pd
from openpyxl import load_workbook
from q1_sales import sales

summary = sales.groupby("Region", as_index=False)["Revenue"].sum()

with pd.ExcelWriter("q1_by_region.xlsx", datetime_format="yyyy-mm-dd") as writer:
    summary.to_excel(writer, sheet_name="Summary", index=False)
    for region, rows in sales.groupby("Region"):
        rows.to_excel(writer, sheet_name=region, index=False)
    print("Engine:", writer.engine)

print(load_workbook("q1_by_region.xlsx").sheetnames)
print(summary.to_string(index=False))

Output:

Engine: xlsxwriter
['Summary', 'Midwest', 'Northeast', 'South', 'West']
   Region  Revenue
  Midwest  5418.00
Northeast 27585.50
    South  9875.25
     West 17431.15
Command Prompt output of pandas ExcelWriter writing a Summary sheet and one sheet per sales region, then listing the sheet names
One workbook, five sheets: Summary plus one sheet per region. The engine line shows XlsxWriter was picked.

The with block saves and closes the file for you. Older tutorials call writer.save(), which pandas removed in 2.0, so stick to the context manager.

Excel showing the q1_by_region.xlsx workbook written by pandas ExcelWriter with Summary, Midwest, Northeast, South and West sheet tabs
The Summary sheet in Excel, with one tab per region along the bottom.

Sheet names can be 31 characters at most, which matters when you build them from data. The groupby aggregation functions guide shows other ways to build the summary.

Append a new sheet to an existing Excel file

Next quarter, the manager wants Q2 targets added to the same workbook. Use mode="a" with the openpyxl engine; XlsxWriter only creates new files:

import pandas as pd
from openpyxl import load_workbook

targets = pd.DataFrame({"Region": ["Midwest", "Northeast", "South", "West"],
                        "Q2 Target": [7500.00, 30000.00, 12000.00, 20000.00]})

with pd.ExcelWriter("q1_by_region.xlsx", mode="a", engine="openpyxl",
                    if_sheet_exists="replace") as writer:
    targets.to_excel(writer, sheet_name="Targets", index=False)

print(load_workbook("q1_by_region.xlsx").sheetnames)
print(pd.read_excel("q1_by_region.xlsx", sheet_name="Targets").to_string(index=False))

Output:

['Summary', 'Midwest', 'Northeast', 'South', 'West', 'Targets']
   Region  Q2 Target
  Midwest       7500
Northeast      30000
    South      12000
     West      20000
Command Prompt output of pandas ExcelWriter in append mode adding a Targets sheet to an existing workbook
The Targets sheet is added after the five sheets that were already there.

if_sheet_exists decides what happens when the sheet name is taken: "error" (the default), "new", "replace" or "overlay". I use replace for scheduled reports so reruns don’t fail.

New orders belong in the DataFrame, not the sheet: add rows to the DataFrame, then write it again.

Set the start row, headers and missing values in to_excel()

Most layout requests are a keyword away. Here I total revenue per rep, add a new hire with no orders, start the table on row 3 and rename the headers:

import pandas as pd
from q1_sales import sales

reps = sales.groupby("Rep", as_index=False)[["Units", "Revenue"]].sum()
reps.loc[len(reps)] = ["Noah Garcia", None, None]          # new hire, no orders yet

reps.to_excel("q1_reps.xlsx", sheet_name="Reps", index=False,
              startrow=2, na_rep="n/a", float_format="%.2f",
              header=["Sales Rep", "Units Sold", "Revenue (USD)"],
              freeze_panes=(3, 0))

print(pd.read_excel("q1_reps.xlsx", skiprows=2).to_string(index=False))

Output:

   Sales Rep  Units Sold  Revenue (USD)
  Ava Miller        31.0        9310.75
Emily Carter        92.0       27585.50
James Wilson        18.0        5418.00
  Liam Davis        27.0        8120.40
Olivia Brown        35.0        9875.25
 Noah Garcia         NaN            NaN

na_rep writes the text n/a into empty cells; read_excel() treats n/a as missing, so it reads back as NaN. float_format="%.2f" rounds floats to two decimals.

Other useful ones are columns=[...] to export a subset, startcol and header=False. If you need to sort the DataFrame by a column, do it before the export.

Write dates to Excel without 00:00:00

pandas stores dates as datetimes, so Excel receives a date plus midnight. The cell format below comes straight from the saved files:

import pandas as pd
from openpyxl import load_workbook
from q1_sales import sales

sales.to_excel("default_dates.xlsx", index=False)
with pd.ExcelWriter("us_dates.xlsx", datetime_format="mm/dd/yyyy") as writer:
    sales.to_excel(writer, index=False)

for name in ("default_dates.xlsx", "us_dates.xlsx"):
    cell = load_workbook(name).active["A2"]
    print(f"{name:<19} {cell.value}  format: {cell.number_format}")

Output:

default_dates.xlsx  2026-01-06 00:00:00  format: YYYY-MM-DD HH:MM:SS
us_dates.xlsx       2026-01-06 00:00:00  format: mm/dd/yyyy

Set datetime_format (or date_format for plain date objects) on ExcelWriter. It’s still a real date, so sorting works.

If the column arrived as text, convert the column to date