Lesson 8.3Lesson 8.3 · Automation & the Everyday Toolkit
Spreadsheets and Documents
openpyxl for Excel and python-docx for Word - read, write, format and template the deliverable paperwork of practice
The BOQ totals itself, the cover letter fills itself, and every report comes out in the same house style - because a script wrote them.
Beneath the drawings, a practice runs on paperwork: bills of quantities, cost sheets, FF&E schedules, area statements, transmittals, cover letters, condition reports. Most of it is structured data poured into a familiar layout - which is precisely what a script produces best.
Two libraries cover the ground. openpyxl reads and writes real Excel .xlsx files - cells, formulas, formatting - so a schedule can be generated, totalled and styled without opening Excel. python-docx builds Word documents - headings, paragraphs, tables - so a report or letter can be templated and filled from project data. Together they automate the deliverable paperwork that quietly eats a designer's week.
openpyxl = the file, not the calculator. Compute in Python. docx: headings/paragraphs(runs)/tables. Template once, pour data.
Reading a spreadsheet - cells, rows and coordinates
openpyxl (pip install openpyxl) reads and writes .xlsx files directly. Its model matches how you already think about a spreadsheet: a workbook holds one or more worksheets, and a worksheet is a grid of cells addressed by a column letter and a row number - A1, B2, C10. Loading an existing file and reading it is direct:
import openpyxl
wb = openpyxl.load_workbook("boq.xlsx", data_only=True)
ws = wb.active
print(ws["A1"].value) # header text
print(ws.cell(row=2, column=2).value)
total = 0
for row in ws.iter_rows(min_row=2, values_only=True):
item, qty, rate = row[0], row[1], row[2]
if qty is not None and rate is not None:
total += qty * rate
print(f"BOQ total: {total}")Two ways to reach a cell, both useful: ws["B2"] by its spreadsheet coordinate, or ws.cell(row=2, column=2) by numeric row and column - the second is what you use inside loops because the numbers can be variables. .value gets the contents. data_only=True asks openpyxl for the last-calculated result of any formula rather than the formula text - important, because openpyxl does not itself evaluate Excel formulas. iter_rows(min_row=2, values_only=True) walks the data rows (skipping the header) and hands you each row as a plain tuple, which is why totalling a BOQ collapses to a simple loop. The if ... is not None guard skips blank cells so a stray empty row does not crash the arithmetic. A real workbook often has several tabs, and openpyxl handles that too: wb.sheetnames lists them and wb["Costs"] selects one by name, so a multi-tab cost or FF&E workbook is just several worksheets you loop over the same way - pick the sheet, then read its grid.
A quick note on file types: openpyxl works with the modern .xlsx format (and .xlsm macro files), not the legacy .xls binary - if you meet an old .xls, open it once in Excel or LibreOffice and re-save as .xlsx, or reach for a different library. For genuinely huge sheets, load_workbook(..., read_only=True) streams rows instead of loading everything into memory, which stops a hundred-thousand-row export from exhausting your machine. For the schedule-sized files most design work involves, the plain load is perfectly fine.
Writing and formatting an Excel file
Generating a spreadsheet is the same model in reverse: make a workbook, write cells, style them, save. You assign to .value (or use append to add a whole row at once), and formatting comes from small style objects - Font, PatternFill, Alignment - attached to cells:
import openpyxl
from openpyxl.styles import Font, PatternFill
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "BOQ"
ws.append(["Item", "Qty", "Rate", "Amount"])
rows = [("Door", 12, 4500), ("Window", 18, 3200)]
for item, qty, rate in rows:
ws.append([item, qty, rate, qty * rate])
head = Font(bold=True, color="FFFFFF")
fill = PatternFill("solid", fgColor="306998")
for cell in ws[1]:
cell.font = head
cell.fill = fill
ws.column_dimensions["A"].width = 24
wb.save("boq_out.xlsx")openpyxl.Workbook() starts a fresh file with one empty sheet, which wb.active returns. ws.append([...]) adds a row and auto-advances - far tidier than tracking row numbers by hand. The style objects are set once and reused: Font(bold=True, color="FFFFFF") (colours are hex RRGGBB strings, white here) and a solid PatternFill in the course python-blue. Looping for cell in ws[1] styles the whole header row. column_dimensions["A"].width widens a column so text is not clipped. You can also write a real Excel formula as a string - ws["D5"] = "=SUM(D2:D4)" - and Excel will calculate it when the file is opened.
Two finishing touches make a generated sheet look genuinely professional rather than scripted. Number formatting is set through a cell's number_format string: cell.number_format = '#,##0' gives thousands separators so an amount reads as 54,000 rather than 54000, and '0.0%' renders a fraction as a percentage. And ws.merge_cells("A1:D1") merges a range into one cell - perfect for a title banner spanning the top of a schedule. You can also freeze the header so it stays visible when scrolling - ws.freeze_panes = "A2" - and switch on an autofilter with ws.auto_filter.ref = ws.dimensions, both one-liners that make a long schedule genuinely usable for whoever opens it. These are exactly the touches you would apply by hand in Excel, expressed once in code and reproduced flawlessly on every run. The result is a real .xlsx a colleague can open, edit and print, produced entirely by script and identical every time.
Workbook -> worksheet -> cells. append() rows. Font/PatternFill for style. Colours are hex RRGGBB strings.
Generating Word documents with python-docx
For letters, reports and specs the deliverable is a Word document, and python-docx (install python-docx, import docx) builds .docx files from code. A document is a sequence of blocks you add in order - headings, paragraphs, tables, images - each through an add_... method:
from docx import Document
from docx.shared import Pt
doc = Document()
doc.add_heading("Site Condition Report", level=0)
doc.add_heading("1. Summary", level=1)
p = doc.add_paragraph("The existing structure is ")
p.add_run("sound").bold = True
p.add_run(" and suitable for reuse.")
table = doc.add_table(rows=1, cols=2)
table.style = "Light Grid Accent 1"
table.rows[0].cells[0].text = "Element"
table.rows[0].cells[1].text = "Condition"
for element, condition in [("Roof", "Good"), ("Walls", "Fair")]:
cells = table.add_row().cells
cells[0].text = element
cells[1].text = condition
doc.save("report.docx")The structure mirrors how you would build the document by hand, top to bottom. add_heading(..., level=0) is the title; level=1 and level=2 are sub-headings that inherit the template's styles. A paragraph is made of runs - stretches of text with the same formatting - which is why bolding one word means splitting it into its own add_run(...) and setting .bold = True. Tables are built by adding a header row, then add_row() per data row and filling cells[i].text - a perfect fit for a schedule or a register. You can also drop in images and page breaks: doc.add_picture("logo.png", width=Inches(2)) (with Inches imported from docx.shared) inserts an image at a set width, and doc.add_page_break() starts a fresh page - enough to assemble a presentable multi-page report entirely from code. doc.save(...) writes a real .docx that opens in Word or Google Docs with live, editable styles.
A word on styles pays off here. Rather than hand-setting fonts and sizes on every paragraph, lean on Word's named styles: add_heading and add_paragraph(..., style="List Bullet") apply the document's built-in styles, so if you start from a branded template the output inherits the studio's typography automatically. That is the difference between a document that merely contains the right words and one that looks like it belongs to your practice - and it costs nothing extra, because the styling lives in the template rather than in your code.
Templating - the real payoff
The transformative move is not generating one document but templating: designing a layout once, then pouring many rows of data through it to produce a consistent document per project, per unit, or per room. The pattern is to read structured data (a spreadsheet or a list of dictionaries) and, in a loop, build one document per record - the template-plus-data-equals-document flow of the figure:
from docx import Document
clients = [
{"name": "Ms Rao", "project": "Villa 12", "fee": 350000},
{"name": "Mr Iyer", "project": "Flat 4B", "fee": 180000},
]
for c in clients:
doc = Document("letter_template.docx")
for para in doc.paragraphs:
para.text = para.text.replace("{name}", c["name"])
para.text = para.text.replace("{project}", c["project"])
para.text = para.text.replace("{fee}", f"{c['fee']:,}")
safe = c["name"].replace(" ", "_")
doc.save(f"letter_{safe}.docx")Here Document("letter_template.docx") opens a Word file you designed by hand - correct fonts, logo, layout - containing placeholders like {name} and {fee}. The loop opens a fresh copy per client, replaces each placeholder with that client's data, and saves a uniquely named file. f"{c['fee']:,}" formats the number with thousands separators (350,000). In minutes you produce a folder of finished, on-brand letters that are impossible to make inconsistently. The same idea reads rows from a BOQ spreadsheet with openpyxl and emits a per-package report with python-docx - the two libraries chained. That chaining is where automation stops being a party trick and becomes real leverage: structured data in, finished, consistent, house-style deliverables out. A caveat to keep you honest: the simple find-and-replace above works when a placeholder sits within a single run; Word sometimes splits text across runs, so for heavier templating a dedicated engine (such as docxtpl) is more robust. Start simple, reach for the engine when the placeholders start misbehaving.
Where the data comes from - chaining the toolkit
None of this lives in isolation. The data you pour into a spreadsheet or a document usually starts somewhere else - a CSV export, a pandas DataFrame from Module 4, a folder you catalogued with last lesson's file tools, or (next lesson) a live API. The everyday toolkit is at its most powerful when chained: read a messy CSV, clean and total it in pandas, write a styled .xlsx with openpyxl, and emit a templated Word summary with python-docx - a single script that turns raw data into several polished, consistent deliverables at once.
import csv
from docx import Document
with open("rooms.csv", newline="") as f:
rooms = list(csv.DictReader(f))
doc = Document()
doc.add_heading("Area Statement", level=0)
total = 0.0
for r in rooms:
area = float(r["area"])
total += area
doc.add_paragraph(f"{r['name']}: {area:.1f} sqm")
doc.add_paragraph(f"Total: {total:.1f} sqm")
doc.save("area_statement.docx")Here csv.DictReader (from Module 3) reads each row as a dictionary keyed by the header, and the single loop both totals the areas and writes one line per room straight into the document - raw table in, finished statement out. Swap the source for an openpyxl read of a BOQ sheet, or a pandas DataFrame, and the shape is unchanged.
A closing note on effort keeps you honest. If a deliverable is genuinely one-off, a script can be slower than simply typing it - the leverage of document automation appears when the same report goes out every month, or when fifty near-identical letters or per-room specs must be produced and kept consistent. For a single unique document, open Word; for a repeating or fan-out deliverable, template it. And keep a human in the loop: generated documents should be reviewed before they leave the office, because a script will just as happily produce fifty perfect copies as fifty copies of the same mistake.
openpyxl
Read and write .xlsx files
Workbook -> worksheet -> cell by coordinate. Reads/writes values, formulas and formatting. Does NOT evaluate formulas itself.
data_only=True
Read cached formula results
load_workbook option that returns Excel's last-saved result of a formula rather than the formula text. Stale if inputs changed since.
python-docx
Build Word .docx documents
Install python-docx, import docx. add_heading / add_paragraph / add_run / add_table. A paragraph is made of runs.
Templating
Layout once, data many times
Fill placeholders in a designed template per record to emit consistent deliverables. Simple replace works; docxtpl is more robust for split runs.
Workshop — the schedule-to-report generator
Build a two-stage tool: generate and total a BOQ as a styled Excel file with openpyxl, then read it back and emit a templated Word summary with python-docx - the read-data, produce-deliverable chain that defines document automation.
Python 3, `pip install openpyxl python-docx`. Optionally Excel or LibreOffice and Word or Google Docs to open and check the outputs.
Goal: generate a styled BOQ .xlsx, then a Word summary from it Inputs: a small list of items (item, qty, rate) Time: ~50 minutes
- 1Install:
pip install openpyxl python-docx. Confirm bothimport openpyxlandfrom docx import Documentrun. - 2Build the BOQ: create a workbook,
appenda header row and one row per item withAmount = qty * ratecomputed in Python, then style the header withFontandPatternFilland widen column A. Saveboq_out.xlsx. - 3Read it back:
load_workbook("boq_out.xlsx"),iter_rowsover the data, and compute the grand total in Python. Print it to confirm. - 4Generate the report: with python-docx add a title, a summary paragraph stating the total, and a table listing each item and amount (header row plus one
add_row()per item). Saveboq_summary.docx. - 5Turn it into a template: extract the client and project name into variables at the top and use them in the heading and summary, so re-running with different inputs produces a correctly-titled document every time.
You’ll walk away with
A script that produces a styled, self-totalling BOQ spreadsheet and, from that same data, a templated Word summary document - demonstrating the read-data, emit-deliverable chain end to end.
Three altitudes on the same idea
Read the band that fits you — or all three.
Transmittals, area statements, condition reports and BOQ summaries are all structured data in a fixed layout. openpyxl can total and format a bill of quantities into a clean, printable .xlsx, and python-docx can generate a templated report or transmittal per issue, on the office letterhead, every time identical. Chain them - read the schedule, emit the report - and a recurring deliverable that took an afternoon takes a keystroke, with no copy-paste transposition errors.
FF&E schedules, finish registers and cost sheets are your spreadsheet life. openpyxl builds and totals them programmatically - one styled row per item, amounts calculated - and can regenerate the whole schedule the moment quantities change. For client-facing specs and proposals, a python-docx template turns a list of selections into a polished, branded document per room. This is the paperwork half of interiors, automated end to end.
Reports, area calculations and submission documents are constant coursework. Automating an area statement or a materials schedule in openpyxl, or generating a formatted report from your data with python-docx, both teaches the libraries and sharpens the deliverables markers actually see. It is also a portable, obviously-useful skill: every office produces documents, and a graduate who can script them is immediately valuable.
“openpyxl can run my Excel formulas, so I can rely on it to calculate my spreadsheet.”
load_workbook a file, change some input numbers with openpyxl, and read a formula cell, you will get the stale result from when Excel last saved, not a freshly computed one - unless you opened it with data_only=True, which gives the cached result and cannot see your new inputs at all. The clean rule: do the arithmetic in Python (you have the numbers, just multiply and sum them) and write final values, or write formula strings and let the user's Excel calculate them on open. Do not expect openpyxl to be Excel; expect it to read and write the file faithfully.Do it yourself
Reason about the data flow before coding.
- 1What are the two ways to address a cell in openpyxl, and which suits a loop?
- 2Why does
data_only=Truematter, and what is its limitation after you change inputs? - 3In python-docx, what is a run and why do you need one to bold a single word?
- 4Describe the templating pattern: how do you turn one layout into fifty consistent documents?
- 5Why is it safer to compute a BOQ total in Python than to rely on openpyxl running an Excel
=SUM()?
The one line to carry out
Peer-reviewed journals & authoritative standards
- 01The Python Standard Library — Python documentation, 2026.
- 02pandas — pandas documentation, 2026.
- 03Comma-separated values — Wikipedia, 2026.
- 04Library (computing) — Wikipedia, 2026.
So far the data has been yours - on disk, in your files. Some of the most useful design data lives online and changes constantly: weather, sun paths, geocoding, material databases. Next we reach out and pull it in, learning what an API is and how the requests library fetches live data into your scripts.
The author
Amogh N P
Architect, interior designer, and creative polymath. Studio Matrx began in his notebooks — his vision of design made honest, useful, and open to everyone. Its Academy is written and taught in his memory, and free, forever.
More about Amogh →