xlsx-processing
Create, parse, format, and audit Microsoft Excel (.xlsx) workbooks programmatically using openpyxl. Use when generating financial spreadsheets with calculated formulas, applying cell styles and number
SKILL.mdskills/xlsx-processing/SKILL.md
GitHubProgrammatic Microsoft Excel (.xlsx) Processing
Excel workbooks (.xlsx) represent structured tabular data, multi-sheet workbooks, formula calculation trees, cell styling, and data formatting. Building production-grade Excel files requires disciplined separation of raw numeric data from display formatting, precise formula syntax, and validation of cell coordinates.
Detailed reference guides:
The 6-Step Spreadsheet Engineering Workflow
- Inspect Requirements: Determine required sheets, data schemas, formula relationships (
SUM,AVERAGE,VLOOKUP), and number formatting (currency, percentages, dates). - Select Tooling: Use
openpyxlfor Python.xlsxgeneration and modification. - Draft / Update Workbook: Write raw typed data (integers, floats, dates, strings) into cells—never format numbers as pre-rendered text strings like
"$1,200.00". - Apply Number Formatting & Styles: Set explicit
number_format, font weights, header fills, column widths, and cell borders. - Validate Calculation Formulas: Confirm formula syntax uses uppercase names, valid coordinate ranges, and produces no
#VALUE!or#REF!errors. - Save & Distribute Clean Artifact: Save to a genuine
.xlsxfile and verify readability.
1. Creating a Styled Financial Model with openpyxl
from datetime import date
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
def generate_financial_model(output_path: str):
wb = Workbook()
ws = wb.active
ws.title = "Q4 Financials"
# Styling Palettes
header_font = Font(name="Calibri", size=11, bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="1F4E79", end_color="1F4E79", fill_type="solid")
total_font = Font(name="Calibri", size=11, bold=True)
border_thin = Side(border_style="thin", color="D9D9D9")
cell_border = Border(top=border_thin, left=border_thin, right=border_thin, bottom=border_thin)
double_bottom = Border(bottom=Side(border_style="double", color="000000"), top=border_thin)
# 1. Document Title
ws["A1"] = "Acme Corp - Q4 Performance Summary"
ws["A1"].font = Font(name="Calibri", size=16, bold=True)
ws["A2"] = f"Report Date: {date.today().isoformat()}"
ws["A2"].font = Font(name="Calibri", size=10, italic=True)
# 2. Table Headers
headers = ["Department", "Budget", "Actual Spend", "Variance", "% Utilized"]
for col_idx, h in enumerate(headers, start=1):
cell = ws.cell(row=4, column=col_idx, value=h)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center" if col_idx > 1 else "left")
# 3. Data Rows
departments = [
("Engineering", 250000, 241200),
("Product & Design", 80000, 78500),
("Sales & Marketing", 140000, 149300),
("Operations & HR", 65000, 61000),
]
start_row = 5
for i, (dept, budget, actual) in enumerate(departments):
r = start_row + i
ws.cell(row=r, column=1, value=dept).border = cell_border
# Numeric values stored as raw numbers (NOT strings)
c_budget = ws.cell(row=r, column=2, value=budget)
c_budget.number_format = "$#,##0"
c_budget.border = cell_border
c_actual = ws.cell(row=r, column=3, value=actual)
c_actual.number_format = "$#,##0"
c_actual.border = cell_border
# Formula: Budget - Actual
c_var = ws.cell(row=r, column=4, value=f"=B{r}-C{r}")
c_var.number_format = "$#,##0"
c_var.border = cell_border
# Formula: Actual / Budget
c_pct = ws.cell(row=r, column=5, value=f"=C{r}/B{r}")
c_pct.number_format = "0.0%"
c_pct.border = cell_border
# 4. Total Summary Row
total_row = start_row + len(departments)
ws.cell(row=total_row, column=1, value="Total").font = total_font
ws.cell(row=total_row, column=1).border = double_bottom
c_tot_b = ws.cell(row=total_row, column=2, value=f"=SUM(B{start_row}:B{total_row-1})")
c_tot_b.font = total_font
c_tot_b.number_format = "$#,##0"
c_tot_b.border = double_bottom
c_tot_a = ws.cell(row=total_row, column=3, value=f"=SUM(C{start_row}:C{total_row-1})")
c_tot_a.font = total_font
c_tot_a.number_format = "$#,##0"
c_tot_a.border = double_bottom
c_tot_v = ws.cell(row=total_row, column=4, value=f"=B{total_row}-C{total_row}")
c_tot_v.font = total_font
c_tot_v.number_format = "$#,##0"
c_tot_v.border = double_bottom
c_tot_p = ws.cell(row=total_row, column=5, value=f"=C{total_row}/B{total_row}")
c_tot_p.font = total_font
c_tot_p.number_format = "0.0%"
c_tot_p.border = double_bottom
# 5. Auto-fit Column Widths with comfortable padding
for col in ws.columns:
max_len = max(len(str(cell.value or "")) for cell in col)
col_letter = get_column_letter(col[0].column)
ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
wb.save(output_path)
print(f"Spreadsheet successfully generated: {output_path}")
if __name__ == "__main__":
generate_financial_model("q4_financials.xlsx")
2. Reading Formulas vs Cached Values
When opening workbooks created by third parties:
load_workbook(filename): Reads the formula text (=SUM(A1:A10)).load_workbook(filename, data_only=True): Reads the cached evaluated value (150.0). (Note: If a formula was generated by a script and never opened in Excel,data_only=Truewill returnNonebecause openpyxl does not evaluate formula engines.)
from openpyxl import load_workbook
# Read formula text
wb_formulas = load_workbook("q4_financials.xlsx", data_only=False)
print("Formula:", wb_formulas["Q4 Financials"]["B9"].value) # Output: =SUM(B5:B8)
# Read evaluated values
wb_values = load_workbook("q4_financials.xlsx", data_only=True)
print("Cached Value:", wb_values["Q4 Financials"]["B9"].value)
3. Common Mistakes and Anti-Patterns
| Anti-Pattern | Bug / Risk | Correct Practice |
|---|---|---|
Writing numbers formatted as strings: "$500" |
Breaks Excel mathematical formulas (#VALUE!) |
Store raw number 500, set number_format = "$#,##0" |
Lowercase or localized formula names: =sum(a1:a5), =SOMME(...) |
Stored text is not guaranteed to parse; localized names fail in the file format | Write English, uppercase function names with comma argument separators: =SUM(A1:A5) |
Writing untrusted strings that start with =, +, -, @ |
openpyxl stores a string beginning with = as a formula (formula injection); exported CSV is risky too |
Force text: cell.value = text; cell.data_type = "s", or validate/escape input |
| Load, edit, save a workbook with charts, images, or pivots | openpyxl does not preserve everything (charts, pivots, some shapes and conditional-format features can be lost) | Test on a copy, diff visually, or edit the XML/use a different tool |
load_workbook(data_only=True) then save() |
Formulas are replaced by cached values permanently | Keep a separate formulas workbook; never save a data_only copy over the source |
Opening .xlsm without keep_vba=True then saving |
Macros dropped | load_workbook(path, keep_vba=True) and save as .xlsm; never run macros |
| Floating-point money in cells | Rounding surprises (Excel keeps about 15 significant digits) | Store cents as integers or use ROUND in formulas |
| Dates as strings | Sorting and arithmetic break | Write datetime.date/datetime objects and set number_format |
| Overwriting open workbooks | File lock crash on Windows (PermissionError) |
Save to new file or ensure workbook is closed |
Relying on data_only=True for newly created files |
Returns None because openpyxl has no calculation engine |
Open in Excel/LibreOffice to calculate caches |
4. Large Files, Recalculation, and Untrusted Workbooks
- Large files:
load_workbook(path, read_only=True)streams rows (close the workbook; no random cell access, formatting limited).Workbook(write_only=True)plusws.append(row)writes with low memory but cannot revisit cells. Both fit exports of tens of thousands of rows or more. - Recalculation: openpyxl never computes formulas, so a script-written workbook has no cached values until a spreadsheet engine opens and saves it. If a downstream reader needs values, recalculate with LibreOffice headless (for example
soffice --headless --convert-to xlsx --outdir out_dir in.xlsx) and inspectdata_only=Trueoutput; or compute values in Python and write them as numbers when formulas are not required. Verify results independently, because engine differences exist. - Error values: after recalculation, scan for
#REF!,#NAME?,#DIV/0!,#VALUE!,#N/Ain cached values. - Untrusted workbooks:
.xlsxis a ZIP of XML. Cap size, avoid external links (xl/externalLinks) and data connections, never enable macros (.xlsm), and consider installingdefusedxmlso openpyxl parses XML defensively. openpyxl does not execute formulas or macros, but Excel will when a person opens the file. - Sharing: remove hidden sheets, comments, and personal properties (
wb.properties) you do not intend to disclose.
5. Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| Cell shows formula text instead of result | Cell stored as text, or number format @ |
Remove text format; ensure value starts with = and is not forced to string |
#NAME? in Excel |
Misspelled or non-English function name, or a function the app lacks | Use English names; avoid newer functions if recipients use older Excel |
data_only=True returns None |
No cached value (never calculated) | Recalculate via a spreadsheet engine |
InvalidFileException |
Wrong extension or format | Use .xlsx/.xlsm/.xltx; convert .xls first |
| Memory blow-up | Full load of a huge file | Use read_only=True / write_only=True |
Column shows #### |
Column too narrow for number or date | Increase width or shorten format |
6. Verification Checklist
- All numeric and financial cells contain raw numeric types (not pre-formatted strings).
- Number formats explicitly set (
$#,##0,0.0%,YYYY-MM-DD). - Formula names in uppercase (
SUM,AVERAGE,IF,VLOOKUP). - Column widths adjusted to prevent
###overflow truncation. - File verified with
zipfile.is_zipfile(path)and opens without error.