Studio Matrx Monthly · Volume 1 · Issue 3 · August 2026
Amogh N P
 In loving memory of Amogh N P — Architect · Designer · Visionary 
Spreadsheets and DocumentsLesson 8.3
PSD for Architecture, Planning & Urban Design/Module 8 · Automation & the Everyday Toolkit

Lesson 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

13 min Interactive lessonFree · open lessonByAmogh N P· Architect & interior designer
The hook

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:

python
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.

A WORKSHEET IS A GRIDABCD1234itemqtyratetotaldoor12450054000window18320057600tile m22409522800ws["B2"] == ws.cell(row=2, column=2) == 12
Zoom
A worksheet is a grid addressed by column letter and row number. openpyxl lets you read or write any cell by its coordinate - ws["B2"] or ws.cell(row=2, column=2) - so a schedule becomes just numbers you can loop over and total.

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:

python
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:

python
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.

TEMPLATE + DATA -> DOCUMENTtemplateDear {client}data rowclient = Raofill blankspython-docxreport.docxone per rowDesign the layout once; the script pours each project's data into a fresh, consistent copy.
Zoom
Templating a document. A fixed template with placeholders plus a row of project data flows through a script that fills the blanks, producing a finished, consistent deliverable - one per row, generated in seconds.

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:

python
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.

python
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.

Tools & terms in this lesson

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.

Hands-on workshop

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.

Given & goal
Goal: generate a styled BOQ .xlsx, then a Word summary from it
Inputs: a small list of items (item, qty, rate)
Time: ~50 minutes
  1. 1Install: pip install openpyxl python-docx. Confirm both import openpyxl and from docx import Document run.
  2. 2Build the BOQ: create a workbook, append a header row and one row per item with Amount = qty * rate computed in Python, then style the header with Font and PatternFill and widen column A. Save boq_out.xlsx.
  3. 3Read it back: load_workbook("boq_out.xlsx"), iter_rows over the data, and compute the grand total in Python. Print it to confirm.
  4. 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). Save boq_summary.docx.
  5. 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.

The worked example

Three altitudes on the same idea

Read the band that fits you — or all three.

For the architectAutomate busywork & build custom tools

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.

For the interior designerScripts for data, schedules & layouts

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.

For the studentA hireable computational skill

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.

Misconception check

openpyxl can run my Excel formulas, so I can rely on it to calculate my spreadsheet.

This is a genuine trap. openpyxl reads and writes the file - including formula text and the last result Excel saved - but it is not a calculation engine and does not evaluate formulas itself. So if you 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.
Try it

Do it yourself

Reason about the data flow before coding.

  1. 1What are the two ways to address a cell in openpyxl, and which suits a loop?
  2. 2Why does data_only=True matter, and what is its limitation after you change inputs?
  3. 3In python-docx, what is a run and why do you need one to bold a single word?
  4. 4Describe the templating pattern: how do you turn one layout into fifty consistent documents?
  5. 5Why is it safer to compute a BOQ total in Python than to rely on openpyxl running an Excel =SUM()?
Take this with you

The one line to carry out

openpyxl reads and writes real Excel files and python-docx builds real Word documents - chain them to turn structured project data into consistent, house-style deliverables on demand. Do the arithmetic in Python; template the layout once and pour data through it.
Take it further
References & further reading

Peer-reviewed journals & authoritative standards

  1. 01The Python Standard LibraryPython documentation, 2026.
  2. 02pandaspandas documentation, 2026.
  3. 03Comma-separated valuesWikipedia, 2026.
  4. 04Library (computing)Wikipedia, 2026.
Related lessons
Recap
A practice's paperwork is structured data in familiar layouts, which scripts produce best. openpyxl reads and writes .xlsx by workbook-worksheet-cell, handling values, formatting and formula strings - but it does not evaluate formulas, so compute in Python. python-docx builds .docx from headings, paragraphs (made of runs) and tables. The real payoff is templating: design a layout once and pour many rows of data through it for consistent deliverables.
Carry forward →

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.

A

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 →