openpyxl: Append Data to an Existing Excel Sheet
You have an existing .xlsx file — a monthly report, a log, a running ledger — and you need to add rows without touching what is already there. The tool for that is openpyxl: open the file with load_workbook(), pick the worksheet, and call ws.append(). The method writes to the first empty row below your data and maps each list item to a column. Because openpyxl edits the Office Open XML directly, appending leaves existing values, formatting, and column widths intact. This guide shows the append patterns you actually need — and the cases where you should index rows explicitly instead. Every example runs: the first step builds the workbook we append to.
Prerequisites
- Python 3.8 or newer and
openpyxlinstalled:pip install openpyxl. - An existing
.xlsx,.xlsm, or.xltx/.xltmfile — or run Step 1 below to create a sample one. Legacy.xlsfiles are not supported and must be converted first. - The target file must be closed in Excel and any other process. An open workbook holds a lock that makes
wb.save()fail. - A working knowledge of the openpyxl load–edit–save loop. If it is new to you, start with Using openpyxl for Excel File Manipulation.
Step 1: Create the workbook to append to
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "Q3_Data"
ws.append(["Date", "Server", "Status", "Note"])
ws.append(["2024-07-12", "Web-01", "Info", "Deploy 4.2"])
ws.append(["2024-07-14", "DB-02", "Warning", "Slow query"])
wb.save("monthly_report.xlsx")
print("Created with", ws.max_row, "rows")
Step 2: Append a single row
Load the file, select the sheet by name, append a list, and save. The list maps to columns A, B, C, D in order:
from openpyxl import load_workbook
wb = load_workbook("monthly_report.xlsx")
ws = wb["Q3_Data"]
ws.append(["2024-07-15", "Server-04", "Critical", "Memory leak resolved"])
wb.save("monthly_report.xlsx")
print("Now", ws.max_row, "rows")
ws.append() always targets the row after the last one that contains data, so you never compute a position yourself for the common case.
Step 3: Append many rows in a loop
For batches, iterate and append. Accumulate the rows in memory first, then write them in one pass:
wb = load_workbook("monthly_report.xlsx")
ws = wb["Q3_Data"]
batch = [
["2024-07-16", "DB-01", "Warning", "Index fragmentation"],
["2024-07-17", "Web-09", "Info", "Routine patch applied"],
["2024-07-18", "Cache-2", "Info", "TTL increased"],
]
for row in batch:
ws.append(row)
wb.save("monthly_report.xlsx")
print("Appended", len(batch), "rows; total", ws.max_row)
Step 4: Append into specific columns with a dict
Pass a dict to write only certain columns, keyed by column letter or 1-based index. Unlisted columns stay empty. This is handy when a row only fills a few fields:
wb = load_workbook("monthly_report.xlsx")
ws = wb["Q3_Data"]
# Only Date (A) and Status (C); Server and Note stay blank
ws.append({"A": "2024-07-19", "C": "Resolved"})
# Same idea using column numbers
ws.append({1: "2024-07-20", 3: "Info"})
wb.save("monthly_report.xlsx")
print("Row 8 values:", [c.value for c in ws[8]])
Step 5: Use the right types
Append native Python types, not strings, for anything you will sort, sum, or pivot later. Dates as datetime/date, numbers as int/float. A date stored as text will not aggregate or format like a real date — to control how a real date displays, set a number format on the cell:
from datetime import date
wb = load_workbook("monthly_report.xlsx")
ws = wb["Q3_Data"]
ws.append([date(2024, 7, 21), "Batch-1", "Info", 1500]) # real date + real int
last_date = ws.cell(row=ws.max_row, column=1)
last_date.number_format = "yyyy-mm-dd"
wb.save("monthly_report.xlsx")
print("Appended typed row at", ws.max_row)
Step 6: Explicit row indexing when append misfires
ws.append() relies on ws.max_row, which tracks the largest used row. If a template carries trailing blank rows, a frozen summary block, or formatting that outruns the data — or rows were deleted (deletion does not always shrink max_row until the file is reopened) — append() can land in the wrong place. Write to explicit coordinates to guarantee placement:
wb = load_workbook("monthly_report.xlsx")
ws = wb["Q3_Data"]
target = ws.max_row + 1
ws.cell(row=target, column=1, value="2024-07-22")
ws.cell(row=target, column=2, value="App-12")
ws.cell(row=target, column=3, value="Resolved")
wb.save("monthly_report.xlsx")
print("Wrote explicit row at", target)
Step 7: Save atomically
A crash or lock during wb.save() can corrupt the target file. Write to a temporary path first, then atomically replace the original with os.replace, which is a single filesystem operation:
import os
from openpyxl import load_workbook
wb = load_workbook("monthly_report.xlsx")
ws = wb["Q3_Data"]
ws.append(["2024-07-23", "Web-03", "Info", "Health check"])
tmp = "monthly_report.tmp.xlsx"
wb.save(tmp)
os.replace(tmp, "monthly_report.xlsx") # atomic on the same filesystem
print("Saved atomically;", ws.max_row, "rows total")
Common pitfalls and gotchas
append()lands too low. The most common surprise: a stalews.max_rowfrom trailing blank rows or deleted-but-not-reopened rows. When placement matters, index explicitly as in Step 6 rather than trustingappend().- Dates and numbers stored as text. Appending
"2024-07-21"as a string gives you a label, not a date — it will not sort, sum, or filter as a date. Append nativedate/datetimeandint/float, and apply the number or date format separately. - Wrong file format. Only
.xlsx,.xlsm, and.xltx/.xltmare supported. A legacy.xlsraisesInvalidFileException— convert it to.xlsxfirst. - Formulas are not evaluated.
append()writes raw values and formula strings verbatim; Excel recalculates formulas when it next opens the file.openpyxlitself never computes them, so reading an appended formula cell back returns the text, not a result. - The file is locked. If the workbook is open in Excel or another process,
wb.save()raisesPermissionError. The atomic-save pattern limits the corruption window but cannot bypass an active lock — close the file first. - Saving overwrites, not merges.
wb.save()writes the whole in-memory workbook. If two scripts load the same file and each save, the last writer wins and the other's appends are lost. Serialize writes or use a lock.
Performance and scale notes
load_workbook() reads the entire file into memory, so appending a handful of rows to a large workbook still pays the full parse-and-rewrite cost on every run. A few ways to keep that manageable:
- Batch your appends. Loading, appending one row, and saving in a tight loop reparses the file each pass. Load once, append the whole batch (Step 3), then save once.
- Stream when generating from scratch. If you are building a large file rather than editing an existing one,
Workbook(write_only=True)streams rows straight to disk and keeps memory flat. Write-only sheets cannot be read back or restyled cell-by-cell afterwards, so use them only for one-shot output. - Reach for pandas on bulk tabular data. When you are appending thousands of rows of structured data rather than a few event lines, writing a DataFrame to Excel is faster to express and to run. Concatenate in pandas, then write once.
- Appending from many source files? If the rows come from a folder of workbooks, combine them into one file in a single pass instead of opening and appending to the target repeatedly.
Where append puts the row
Conclusion
ws.append() is the right tool when you can trust that ws.max_row reflects the real last-used row: load the workbook, append lists or dicts, use native types, and save. For templates with trailing blank rows or deleted-row artifacts, skip it and write to explicit coordinates with ws.cell(row=target, column=n) instead. Either way, wrap the save in an atomic write-then-replace to protect against a corrupt file if the process is interrupted mid-save.
Frequently asked questions
Where does ws.append() put the new row?
It writes to the first row after ws.max_row, the largest used row, and maps each list item to columns A, B, C, and so on. You never compute the position yourself in the common case.
Why did append() land in the wrong place?append() trusts ws.max_row. Trailing blank rows, a frozen summary block, or rows deleted without reopening the file can leave max_row stale, so the row lands too low. Write to explicit ws.cell(row=target, column=n) coordinates instead.
Can I append into only some columns?
Yes. Pass a dict keyed by column letter ({"A": ..., "C": ...}) or 1-based index ({1: ..., 3: ...}). Unlisted columns stay empty for that row.
Why won't my appended dates sort or sum like dates?
You probably appended them as strings. Append native datetime/date and int/float values, then set number_format on the cell for display — text values won't aggregate or format as real dates.
How do I avoid corrupting the file if the save is interrupted?
Save to a temporary path, then os.replace(tmp, target) to swap it in atomically on the same filesystem. This limits the corruption window but cannot bypass an active file lock, which still raises PermissionError.
Related
- Up: Using openpyxl for Excel File Manipulation — the full toolkit: styling, column widths, formulas, and images.
- Read a Cell Value from Excel with openpyxl — the reading half of the load–edit–save loop.
- Writing DataFrames to Excel with Pandas — when you are appending large tabular datasets rather than a few rows.
- Combine Multiple Excel Files into One in Python — appending data sourced from many files.
- Format Dates in Excel Cells with Python — make appended
datevalues display the way you want.