Write a DataFrame to a Specific Cell in Excel with pandas
df.to_excel() puts the header in cell A1 by default, which is right for a data dump and wrong for almost any report. Reports have a title in the top rows, a date or a note beside the table, sometimes two tables on one sheet — and very often an existing template whose layout someone designed by hand. pandas handles all of these with two arguments, startrow and startcol, plus one writer setting that decides whether the rest of the sheet survives. This guide is part of Writing DataFrames to Excel with Pandas.
Prerequisites
pip install pandas openpyxl xlsxwriter
import pandas as pd
sales = pd.DataFrame({
"region": ["North", "South", "East"],
"units": [410, 388, 295],
"revenue": [12300.0, 11640.0, 8850.0],
})
Step 1 — Offset the table in a new file
startrow and startcol are zero-based offsets from A1. To leave four rows for a title block and one blank column on the left, put the header at B5:
with pd.ExcelWriter("monthly_report.xlsx", engine="xlsxwriter") as writer:
sales.to_excel(writer, sheet_name="Report", startrow=4, startcol=1, index=False)
sheet = writer.sheets["Report"]
sheet.write("A1", "Monthly sales report", writer.book.add_format({"bold": True, "font_size": 14}))
sheet.write("E2", "Prepared by ops")
Writing through an ExcelWriter rather than a file name is what lets you add the title afterwards: writer.sheets gives you the engine's worksheet object for the sheet pandas just created, and writer.book the workbook. With the xlsxwriter engine you can then use its whole API — formats, merged titles, column widths — on the same sheet in the same pass.
Converting an Excel address to offsets is easy to get wrong by one, so let openpyxl do it:
from openpyxl.utils.cell import coordinate_from_string, column_index_from_string
def offsets(address: str) -> tuple[int, int]:
letters, row = coordinate_from_string(address)
return row - 1, column_index_from_string(letters) - 1
print(offsets("B5")) # (4, 1)
print(offsets("AA10")) # (9, 26)
Step 2 — Write into an existing template without wiping it
The more common case is a template that already exists: a designed header, a logo, notes, fixed column widths. Opening it with the default writer mode would create a new file and throw all of that away. Append mode with the openpyxl engine and if_sheet_exists="overlay" writes only the cells the DataFrame covers:
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font
template = Workbook()
ws = template.active
ws.title = "Report"
ws["A1"] = "Monthly sales report"
ws["A1"].font = Font(bold=True, size=14)
ws["E2"] = "Prepared by ops"
ws.column_dimensions["B"].width = 18
template.save("template.xlsx")
with pd.ExcelWriter("template.xlsx", mode="a", engine="openpyxl", if_sheet_exists="overlay") as writer:
sales.to_excel(writer, sheet_name="Report", startrow=4, startcol=1, index=False)
check = load_workbook("template.xlsx")["Report"]
print(check["A1"].value, check["A1"].font.b) # Monthly sales report True
print(check["B5"].value, check["C6"].value) # region 410
print(check.column_dimensions["B"].width) # 18.0
The title, its font, the note and the column width all survive. That is the whole difference between the if_sheet_exists options:
overlay also means stale values remain. If last month's table had five rows and this month's has three, rows four and five from last month are still there underneath. Clear the data area before writing, as in Step 4.
Step 3 — Fill a designed header with values only
Templates often have the header row already styled — merged, coloured, maybe with line breaks in the labels. Write just the values under it with header=False, starting one row lower:
with pd.ExcelWriter("template.xlsx", mode="a", engine="openpyxl", if_sheet_exists="overlay") as writer:
sales.to_excel(writer, sheet_name="Report", startrow=5, startcol=1, index=False, header=False)
The designed header in row 5 is untouched and the values land from B6. This is the core move of template-driven reporting; the full workflow, including keeping charts and images alive, is in Populate an Excel Template Without Losing Formatting.
Step 4 — Clear the old data area first
Because overlay only writes where the new frame has cells, clear the whole data region before every run. openpyxl can blank a block while keeping cell formatting:
def clear_block(path: str, sheet: str, first_row: int, first_col: int, n_cols: int) -> None:
wb = load_workbook(path)
ws = wb[sheet]
for row in ws.iter_rows(min_row=first_row, max_row=ws.max_row,
min_col=first_col, max_col=first_col + n_cols - 1):
for cell in row:
cell.value = None
wb.save(path)
clear_block("template.xlsx", "Report", first_row=6, first_col=2, n_cols=3) # B6 downwards
with pd.ExcelWriter("template.xlsx", mode="a", engine="openpyxl", if_sheet_exists="overlay") as writer:
sales.head(2).to_excel(writer, sheet_name="Report", startrow=5, startcol=1, index=False, header=False)
report = load_workbook("template.xlsx")["Report"]
print([report.cell(row=r, column=2).value for r in range(6, 10)]) # ['North', 'South', None, None]
Setting cell.value = None keeps the cell's number format, fill and borders, so the designed banding of the data area stays in place for the next run.
Step 5 — Stack several tables on one sheet
Offsets also let you place several frames on one sheet, one under another. Track the next free row as you go, leaving a gap and a caption between tables:
by_region = sales.set_index("region")
top = sales.nlargest(2, "revenue")
with pd.ExcelWriter("stacked.xlsx", engine="xlsxwriter") as writer:
row = 0
for caption, frame in [("By region", by_region), ("Top two", top)]:
frame.to_excel(writer, sheet_name="Summary", startrow=row + 1)
writer.sheets["Summary"].write(row, 0, caption)
row += len(frame) + 4 # caption + header + rows + gap
len(frame) counts data rows only, so add one for the header and one for the caption line; a MultiIndex header takes more rows, as Write a MultiIndex DataFrame to Excel with pandas explains. Side-by-side layouts work the same way with startcol — the pattern is covered in Write Multiple DataFrames to One Excel File.
Step 6 — Anchor the table to a named cell, not a hard-coded address
Hard-coding startrow=5, startcol=1 couples your script to the template's layout. The day someone adds a row to the title block, every run writes one row too high, over the header. A sturdier contract is a defined name in the template: whoever designs the sheet marks the top-left data cell as data_start, and the script looks it up.
from openpyxl.workbook.defined_name import DefinedName
wb = load_workbook("template.xlsx")
wb.defined_names["data_start"] = DefinedName("data_start", attr_text="Report!$B$6")
wb.save("template.xlsx")
def anchor(path: str, name: str) -> tuple[str, int, int]:
book = load_workbook(path)
sheet_name, ref = next(iter(book.defined_names[name].destinations))
return sheet_name, *offsets(ref.replace("$", ""))
sheet_name, start_row, start_col = anchor("template.xlsx", "data_start")
print(sheet_name, start_row, start_col) # Report 5 1
with pd.ExcelWriter("template.xlsx", mode="a", engine="openpyxl", if_sheet_exists="overlay") as writer:
sales.to_excel(writer, sheet_name=sheet_name, startrow=start_row, startcol=start_col,
index=False, header=False)
When the template designer inserts rows above the table in Excel, Excel moves the defined name with the cell, so the script keeps writing to the right place without a code change. It also documents intent: a reader of the template can open Name Manager and see exactly where automation writes. Named ranges are covered in more depth in Create a Named Range in Excel with openpyxl.
Common pitfalls
- One-based offsets.
startrow=5puts the header in row 6. Use theoffsets()helper instead of converting in your head. mode="a"with xlsxwriter. xlsxwriter only creates new files, so append mode requires the openpyxl engine; pandas raises if you combine them.- The index column shifts everything. With the default
index=True, the first DataFrame column lands one column right ofstartcol. Passindex=Falseor allow for it. - Overlay leaves stale rows. Clear the data area first when the row count can shrink between runs.
Performance and scale notes
Append mode loads the whole existing workbook with openpyxl, edits it in memory and saves it again. For a template of a few sheets that takes a fraction of a second; for a workbook with hundreds of thousands of existing cells it can take many seconds and a lot of memory, and saving through openpyxl can drop features it does not support. When the template is large, keep the template itself small — layout and formatting only — and put bulk data on a separate sheet written in one pass with xlsxwriter.
Conclusion
startrow and startcol place a DataFrame anywhere, counting from zero at A1. For a new file, write through an ExcelWriter and decorate the sheet with the engine's API in the same pass. For an existing template, use mode="a", the openpyxl engine and if_sheet_exists="overlay", write values only under a designed header, and clear the data area first so shrinking tables do not leave stale rows behind.
Frequently asked questions
Are startrow and startcol zero-based? Yes. startrow=0 and startcol=0 is cell A1, so to put the header in cell B5 you pass startrow=4 and startcol=1.
How do I write into an existing sheet without deleting what is already there? Open the file with pd.ExcelWriter in mode="a" with the openpyxl engine and if_sheet_exists="overlay". pandas then writes only the cells covered by the DataFrame and leaves the rest of the sheet, including formatting, as it was.
What is the difference between overlay and replace? replace deletes the existing sheet and creates a fresh empty one with the same name before writing, so titles, notes and column widths are lost. overlay writes into the existing sheet.
Can I write only the values, without the header? Yes. Pass header=False, and index=False if you do not want the index either. This is how you fill the data area of a template whose header row is already designed.
Related
- Up one level: Writing DataFrames to Excel with Pandas — the writer options in context.
- Append a DataFrame to an Existing Excel File with pandas — adding rows below existing data.
- Fill an Excel Template with Python and openpyxl — cell-by-cell template filling.
- Write a Pandas DataFrame to Excel Without the Index — keep the index out of the offset maths.
More on writing excel files
- How-toWrite Multiple DataFrames to One Excel File
- How-toWrite a Formatted Excel Report with xlsxwriter
- How-toFind and Replace Text in Excel with openpyxl
- How-toInsert and Delete Rows and Columns with openpyxl
- How-toopenpyxl: Append Data to an Existing Excel Sheet
- How-toReorder Columns to Match a Template Layout with pandas