XLSX Recipes — openpyxl
Contents
- Create with formatting (styles, header row, merged cells, column widths)
- Charts
- Read / extract (all sheets, one sheet, used range, tables)
- Modify (insert/delete rows and columns, find-and-replace, add a sheet)
- Convert (CSV ↔ xlsx, xlsx → pandas)
- Gotchas
Create with formatting
python
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
wb = Workbook()
ws = wb.active
ws.title = "Report"
# Header row
headers = ["Region", "Q1", "Q2", "Total"]
ws.append(headers)
header_font = Font(name="Arial", bold=True, color="FFFFFF")
header_fill = PatternFill("solid", fgColor="4472C4")
for cell in ws[1]:
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center")
# Data + formula per row
for row in [["East", 100, 150], ["West", 90, 120]]:
ws.append(row)
for r in range(2, ws.max_row + 1):
ws.cell(row=r, column=4).value = f"=SUM(B{r}:C{r})"
# Merged title above the table: insert row first, then merge
ws.insert_rows(1)
ws["A1"] = "Quarterly Sales"
ws.merge_cells("A1:D1")
ws["A1"].font = Font(size=14, bold=True)
# Column widths (openpyxl never auto-sizes)
for col in range(1, 5):
ws.column_dimensions[get_column_letter(col)].width = 14
# Number format
for r in range(3, ws.max_row + 1):
for c in range(2, 5):
ws.cell(row=r, column=c).number_format = "#,##0"
wb.save("report.xlsx")Charts
python
from openpyxl.chart import BarChart, Reference
chart = BarChart()
chart.title = "Revenue by Region"
data = Reference(ws, min_col=2, max_col=3, min_row=2, max_row=ws.max_row) # includes header row for series names
cats = Reference(ws, min_col=1, min_row=3, max_row=ws.max_row)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
ws.add_chart(chart, "F3") # anchor = top-left cell of the chart
wb.save("report.xlsx")Read / extract
python
from openpyxl import load_workbook
wb = load_workbook("report.xlsx", data_only=True)
# All sheets → list of rows
for name in wb.sheetnames:
ws = wb[name]
rows = [[c.value for c in row] for row in ws.iter_rows()]
# Used range only (skips trailing empty rows/cols)
ws = wb["Report"]
data = [[c.value for c in row] for row in ws.iter_rows(min_row=1, max_row=ws.max_row, max_col=ws.max_column)]
# Streaming read for large files
wb_big = load_workbook("big.xlsx", read_only=True, data_only=True)
for row in wb_big["Sheet1"].iter_rows(values_only=True):
pass # process row tuple
wb_big.close() # read_only keeps the file handle open — always closeModify
python
from openpyxl import load_workbook
wb = load_workbook("report.xlsx")
ws = wb["Report"]
# Insert / delete
ws.insert_rows(2) # one row above row 2
ws.delete_cols(3) # delete column C
ws.insert_cols(3, amount=2)
# Find-and-replace (string cells only)
for row in ws.iter_rows():
for cell in row:
if isinstance(cell.value, str) and "East" in cell.value:
cell.value = cell.value.replace("East", "North-East")
# Add a sheet at a position
summary = wb.create_sheet("Summary", 0) # index 0 = first tab
summary["A1"] = "=Report!D3"
wb.save("report.xlsx")Insert/delete shifts cells but does not rewrite formulas that referenced the shifted range — check formulas after structural edits.
Convert
python
# CSV → xlsx
import csv
from openpyxl import Workbook
wb = Workbook(); ws = wb.active
with open("data.csv", newline="") as f:
for row in csv.reader(f):
ws.append(row)
wb.save("data.xlsx")
# xlsx → CSV (one sheet)
import csv
from openpyxl import load_workbook
ws = load_workbook("data.xlsx", data_only=True).active
with open("out.csv", "w", newline="") as f:
csv.writer(f).writerows([c if c is not None else "" for c in row]
for row in ws.iter_rows(values_only=True))
# Bulk I/O escape hatch
import pandas as pd
df = pd.read_excel("data.xlsx", sheet_name="Report") # needs openpyxl installed
df.to_excel("out.xlsx", index=False) # loses all formulas/formattingFormula recalculation without Excel: soffice --headless --convert-to xlsx --outdir /tmp file.xlsx (LibreOffice recalculates and writes cached values).
Gotchas
- openpyxl never computes formulas.
data_only=Truereturns the cached value from the last save by Excel/LibreOffice; a file created by openpyxl and never opened elsewhere has no cached values (None). - Loading with
data_only=Trueand saving destroys all formulas — they are replaced by their cached values. Never re-save a data_only load. ws.max_row/max_columncount formatted-but-empty cells, so they can overshoot the real data; trim trailingNonerows when extracting.- Merged cells: only the top-left cell holds the value; the rest read
None. Unmerge withws.unmerge_cells(...)before editing the range. - Dates come back as
datetimeobjects; number formats (e.g."YYYY-MM-DD") control display only. - Colors are 8-digit ARGB hex (
"FF4472C4") or 6-digit RGB — no#prefix. keep_vba=Trueis required to round-trip.xlsm; saving an.xlsmload as.xlsxdrops macros without warning.- Styles are copied by assignment, not reference: to reuse a style on many cells, assign
Font(...)/PatternFill(...)objects per cell or useNamedStyle.