Guide
Write Multiple DataFrames to One Excel File
Write several pandas DataFrames into one .xlsx: separate sheets via one ExcelWriter, stacking on a single sheet with startrow, and appending with mode='a'.
You have several DataFrames and want them in one workbook — one sheet each, or stacked on a single sheet, or appended to a file that already exists. The mechanism for all three is pandas.ExcelWriter, and the single rule that prevents the most common failure is: one writer per file, opened once. This page is part of Building Multi-Sheet Excel Dashboards; it covers the writer mechanics that the dashboard build depends on.
Prerequisites
You need pandas and at least one Excel engine. openpyxl reads and writes .xlsx and supports append mode, so install it:
pip install pandas openpyxl xlsxwriter
xlsxwriter is optional — it is a fast write-only engine, useful when you never need to reopen the file. For multi-sheet dashboards you reopen later, prefer openpyxl.
One sheet per DataFrame
Open a single pd.ExcelWriter as a context manager and call to_excel() once per DataFrame, each with a distinct sheet_name. The with block writes and closes the file exactly once when it exits:
import pandas as pd
region = pd.DataFrame({
"region": ["North", "South", "West"],
"revenue": [26700, 20900, 7600],
})
month = pd.DataFrame({
"month": ["Jan", "Feb", "Mar"],
"revenue": [22300, 21800, 11100],
})
product = pd.DataFrame({
"product": ["Widget", "Gadget", "Gizmo"],
"units": [350, 209, 76],
})
with pd.ExcelWriter("report.xlsx", engine="openpyxl") as writer:
region.to_excel(writer, sheet_name="By Region", index=False)
month.to_excel(writer, sheet_name="By Month", index=False)
product.to_excel(writer, sheet_name="By Product", index=False)
print("Wrote 3 sheets to report.xlsx")
The order of the to_excel() calls sets the left-to-right tab order. index=False drops the pandas row-number column — pass it on every call, because it is not remembered between writes.
Several DataFrames on the same sheet
To place tables side by side or stacked on one sheet, pass the same sheet_name with different startrow / startcol offsets. startrow and startcol are zero-based and count from the top-left. Leave a blank row between stacked tables so they read as separate blocks:
import pandas as pd
summary = pd.DataFrame({
"Metric": ["Total Revenue", "Total Units"],
"Value": [55200, 635],
})
detail = pd.DataFrame({
"region": ["North", "South", "West"],
"revenue": [26700, 20900, 7600],
})
with pd.ExcelWriter("stacked.xlsx", engine="openpyxl") as writer:
summary.to_excel(writer, sheet_name="Overview", index=False, startrow=0)
# leave one blank row after summary (len + header + gap)
next_row = len(summary) + 2
detail.to_excel(writer, sheet_name="Overview",
index=False, startrow=next_row)
print(f"Stacked detail starting at row {next_row + 1} on the Overview sheet")
For side-by-side blocks, keep startrow fixed and vary startcol instead — for example startcol=0 for the first table and startcol=4 for the second.
Append a sheet to an existing workbook
To add a sheet to a file you already wrote — without rewriting the whole thing — open the writer with mode="a" (append) and an if_sheet_exists policy. Append mode requires engine="openpyxl"; xlsxwriter cannot append. if_sheet_exists controls what happens when the sheet name is already present:
import pandas as pd
# Assume report.xlsx already exists from the first example.
late = pd.DataFrame({
"region": ["East"],
"revenue": [4100],
})
with pd.ExcelWriter("report.xlsx", engine="openpyxl",
mode="a", if_sheet_exists="replace") as writer:
late.to_excel(writer, sheet_name="By Region East", index=False)
print("Appended a sheet to the existing report.xlsx")
The if_sheet_exists values are: "error" (default — raise if the name exists), "replace" (drop and rewrite the sheet), "overlay" (write on top of existing cells, keeping the rest), and "new" (create a uniquely renamed sheet). Use "replace" for idempotent jobs that rerun the same report.
Choosing an engine
| Engine | Read existing file | Append mode | Charts | When to use |
|---|---|---|---|---|
openpyxl | Yes | Yes | Yes (openpyxl.chart) | Multi-sheet dashboards you reopen to add charts, links, or sheets. |
xlsxwriter | No (write-only) | No | Yes (workbook.add_chart) | One-shot writes where speed matters and you never reopen the file. |
For everything in this cluster, use openpyxl — append and load_workbook() are what make the dashboard build work.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Only the last sheet survives | A second pd.ExcelWriter opened on the same path in default mode="w" truncated the file | Keep all writes in one with block, or use mode="a" to add to an existing file. |
A stray 0, 1, 2 column appears | index=False missing — it is per-call, not global | Pass index=False on every to_excel() call. |
InvalidWorksheetName or silent truncation | Sheet name over 31 characters | Keep names ≤ 31 chars; truncate long names before writing. |
ValueError on the sheet name | Name contains \ / * ? : [ ] | Strip those characters; Excel forbids them in tab names. |
ValueError: Append mode is not supported with xlsxwriter | mode="a" with the wrong engine | Use engine="openpyxl" for append. |
| Appended sheet raises on rerun | Default if_sheet_exists="error" and the name already exists | Pass if_sheet_exists="replace" for idempotent reruns. |
Scale note
For workbooks with many large sheets, xlsxwriter writes faster and uses less memory than openpyxl, and its constant_memory mode streams rows to disk — but it cannot reopen or append, so you must write every sheet in one pass. If a job grows beyond comfortable memory, write each DataFrame to its own file and combine them in a final openpyxl pass, or switch the whole job to a single streaming xlsxwriter write.
Frequently asked questions
Why does my file only contain the last DataFrame?
You opened more than one ExcelWriter on the same path. The second one opens in write mode and truncates the file. Put every to_excel() call inside a single with pd.ExcelWriter(...) block.
Do I need index=False on every call?
Yes. index=False is an argument to each to_excel() call and is not carried over from a previous write. Omit it once and that sheet gets a row-number column.
What is the maximum sheet name length?
Excel caps tab names at 31 characters and forbids \ / * ? : [ ]. Names over the limit or with those characters raise an error or get mangled — sanitize names you build from data.
How do I add a sheet without rewriting the file?
Open the writer with mode="a" and engine="openpyxl", and set if_sheet_exists to control collisions. Use "replace" so a rerun overwrites the sheet cleanly.
Conclusion
Writing multiple DataFrames to one file comes down to one ExcelWriter: distinct sheet_name values for separate tabs, matching sheet_name with startrow/startcol offsets to stack tables on one sheet, and mode="a" with if_sheet_exists to append to an existing workbook. Open the writer once, pass index=False every time, and keep sheet names short and clean — and the three failure modes above never occur.
Where to go next
- Building Multi-Sheet Excel Dashboards — assemble these sheets into a Summary-plus-detail dashboard with a chart.
- Add a Summary Sheet to an Excel Report with Python — compute KPIs and write a front-page Summary tab.
- Writing DataFrames to Excel with Pandas — the core
to_excel()options these examples build on.