Add a Total Row to an Excel Table with Python
Excel tables can show a totals row: a last row under the data, styled with the table, where each column has a dropdown to pick Sum, Average, Count, Min, Max and so on. It is better than a hand-written SUM under the data because it is part of the table — it moves when rows are added, respects filters and keeps its styling. Adding one from Python takes three pieces that must agree: the table's range, the column definitions and the formulas in the cells. This guide, part of Creating Excel Tables and Autofilters with Python, puts them together with openpyxl and XlsxWriter. Creating the table itself is covered in Create an Excel Table with Python.
Prerequisites
pip install openpyxl xlsxwriter pandas
Step 1 — Leave room for the totals row in the range
A table's ref covers the header, the data and the totals row. With four data rows under a header in row 1, the data ends in row 5 and the totals row is row 6:
from openpyxl import Workbook
from openpyxl.worksheet.filters import AutoFilter
from openpyxl.worksheet.table import Table, TableColumn, TableStyleInfo
wb = Workbook()
ws = wb.active
ws.title = "Sales"
ws.append(["Region", "Orders", "Amount"])
for row in [("North", 12, 1200.5), ("South", 8, 950), ("East", 15, 1780.25), ("West", 5, 430)]:
ws.append(row)
last_data = ws.max_row # 5
totals_row = last_data + 1 # 6
The filter buttons, though, belong to the header and data only. Excel sets the table's autofilter range to exclude the totals row, and doing the same avoids a table whose filter includes its own total.
Step 2 — Define the columns with their totals
Each TableColumn needs the exact header text as its name — a mismatch makes Excel repair the file — plus either a label or a function for the totals row. openpyxl can generate columns automatically, but at the point you configure them it only knows placeholder names like "Column1", so build them from the header cells:
headers = [cell.value for cell in ws[1]]
columns = [TableColumn(id=i, name=name) for i, name in enumerate(headers, start=1)]
columns[0].totalsRowLabel = "Total"
columns[1].totalsRowFunction = "sum"
columns[2].totalsRowFunction = "sum"
table = Table(displayName="Sales", ref=f"A1:C{totals_row}", totalsRowCount=1,
tableColumns=columns, autoFilter=AutoFilter(ref=f"A1:C{last_data}"))
table.tableStyleInfo = TableStyleInfo(name="TableStyleMedium2", showRowStripes=True)
totalsRowFunction accepts sum, average, count, countNums, max, min, stdDev, var and custom. It is what Excel's dropdown shows as selected; the reader can switch it to another function in Excel afterwards.
Step 3 — Write the formulas into the totals row
openpyxl records the function but does not write the formula — the cells would stay empty until someone reselects the function in Excel. Write what Excel itself would write: SUBTOTAL with function number 109 for a sum, using structured references to the table's columns:
ws[f"A{totals_row}"] = "Total"
ws[f"B{totals_row}"] = "=SUBTOTAL(109,Sales[Orders])"
ws[f"C{totals_row}"] = "=SUBTOTAL(109,Sales[Amount])"
ws.add_table(table)
wb.save("sales_table.xlsx")
print(table.ref, table.totalsRowCount) # A1:C6 1
Recalculating the saved file in LibreOffice gives 40 orders and 4,360.75. The 100-series function numbers ignore rows hidden by a filter, so when a reader filters to North and South, the totals show 20 and 2,150.50 — the total of what they are looking at. Structured references such as Sales[Amount] keep working when rows are added inside the table; they are covered in Use Structured Table References in Formulas from Python.
Step 4 — A custom total
Some totals are not a single function of one column — the average order value, for example, is total amount divided by total orders. Use custom, store the formula on the column, and write the same formula in the cell:
from openpyxl.worksheet.table import TableFormula
wb = Workbook()
ws = wb.active
ws.title = "Sales"
ws.append(["Region", "Orders", "Amount", "Per order"])
for i, (region, n, amount) in enumerate([("North", 12, 1200.5), ("South", 8, 950),
("East", 15, 1780.25), ("West", 5, 430)], start=2):
ws.append([region, n, amount, f"=C{i}/B{i}"])
cols = [TableColumn(id=i, name=h) for i, h in enumerate(["Region", "Orders", "Amount", "Per order"], 1)]
cols[0].totalsRowLabel = "Total"
cols[1].totalsRowFunction = cols[2].totalsRowFunction = "sum"
custom = "SUBTOTAL(109,Sales[Amount])/SUBTOTAL(109,Sales[Orders])"
cols[3].totalsRowFunction = "custom"
cols[3].totalsRowFormula = TableFormula(attr_text=custom)
ws.add_table(Table(displayName="Sales", ref="A1:D6", totalsRowCount=1, tableColumns=cols,
autoFilter=AutoFilter(ref="A1:D5")))
ws["A6"], ws["B6"], ws["C6"] = "Total", "=SUBTOTAL(109,Sales[Orders])", "=SUBTOTAL(109,Sales[Amount])"
ws["D6"] = "=" + custom
wb.save("sales_custom.xlsx")
The total per order is 4,360.75 ÷ 40 = 109.02 — a weighted average, which is what the row really means. Averaging the per-order column instead would give each region equal weight regardless of size, the mistake explained in Use Different Aggregations per Column in a pandas Pivot.
Step 5 — The same with XlsxWriter
XlsxWriter's add_table builds the totals row in one step, including the formulas, from a total_row flag and per-column settings:
import xlsxwriter
book = xlsxwriter.Workbook("sales_xw.xlsx")
sheet = book.add_worksheet("Sales")
data = [["North", 12, 1200.5], ["South", 8, 950], ["East", 15, 1780.25], ["West", 5, 430]]
sheet.add_table("A1:C6", {
"name": "Sales", "data": data, "total_row": True,
"columns": [{"header": "Region", "total_string": "Total"},
{"header": "Orders", "total_function": "sum", "total_value": 40},
{"header": "Amount", "total_function": "sum", "total_value": 4360.75}],
})
book.close()
print("written")
The range again includes the totals row. total_value stores the result so readers that do not recalculate see the right figure, for the reasons covered in Store Formula Results with xlsxwriter So Every Viewer Shows Values.
Step 6 — Keep the totals row out of pandas
pd.read_excel has no idea the last row is a total. Read only the data rows by counting the table's rows:
import pandas as pd
from openpyxl import load_workbook
tbl = load_workbook("sales_table.xlsx")["Sales"].tables["Sales"]
first, last = tbl.ref.split(":")
data_rows = int(last[1:]) - int(first[1:]) - (tbl.totalsRowCount or 0)
df = pd.read_excel("sales_table.xlsx", sheet_name="Sales", nrows=data_rows)
print(len(df), df["Amount"].sum()) # 4 4360.75
Without the adjustment the frame has a fifth row whose Amount doubles the sum. A reusable reader that handles headers, totals and table names is in Read an Excel Table by Name into pandas.
Step 7 — Add rows to a table that has totals
Appending a row with ws.append puts it below the totals row, outside the table. To add data to an existing table, write the new rows where the totals row is, move the totals row down, and extend both ranges:
wb = load_workbook("sales_table.xlsx")
ws = wb["Sales"]
tbl = ws.tables["Sales"]
old_total = int(tbl.ref.split(":")[1][1:]) # 6
new_rows = [("Central", 9, 870.0), ("Islands", 3, 215.5)]
for offset, row in enumerate(new_rows):
for col, value in enumerate(row, start=1):
ws.cell(old_total + offset, col, value)
new_total = old_total + len(new_rows)
ws[f"A{new_total}"] = "Total"
ws[f"B{new_total}"] = "=SUBTOTAL(109,Sales[Orders])"
ws[f"C{new_total}"] = "=SUBTOTAL(109,Sales[Amount])"
tbl.ref = f"A1:C{new_total}"
tbl.autoFilter.ref = f"A1:C{new_total - 1}"
wb.save("sales_table.xlsx")
print(tbl.ref, tbl.autoFilter.ref) # A1:C8 A1:C7
The structured references in the totals formulas need no change: Sales[Amount] now covers the longer column automatically. What openpyxl will not do is update the ranges for you — insert_rows shifts cells but leaves the table's ref where it was — so always set both ranges after changing the data.
Common pitfalls
- ref without the extra row. If the range ends at the last data row, the "totals row" overwrites your data's last line in Excel's view.
- Column names that differ from the header. "Amount " with a space in the cell and "Amount" in the column definition make Excel repair the table.
- Two tables with the same name.
displayNamemust be unique in the workbook; when writing one table per sheet, derive the name from the sheet. - SUM instead of SUBTOTAL. A plain
SUMignores filters and double-counts if another total sits inside the range.
Conclusion
A table totals row needs three things that agree: a ref one row longer than the data with totalsRowCount=1 and an autofilter that stops above it, TableColumn definitions named exactly like the headers with totalsRowFunction or totalsRowLabel, and SUBTOTAL(109, Table[Column]) formulas written into the cells — or custom with a matching totalsRowFormula. XlsxWriter does the formulas for you in new files. When reading the table back, drop the totals row before summing.
Frequently asked questions
How do I add a total row to an Excel table with openpyxl? Create the Table with a ref that includes one extra row and totalsRowCount=1, define each TableColumn with totalsRowFunction (such as "sum") or totalsRowLabel, and write the matching SUBTOTAL formulas into the totals row cells yourself.
Why use SUBTOTAL(109, …) instead of SUM? SUBTOTAL with function number 109 ignores rows hidden by a filter, so the total follows what the reader has filtered. It is also what Excel itself writes into a table's totals row.
Do I have to write the formulas if I set totalsRowFunction? With openpyxl, yes. totalsRowFunction tells Excel which function the totals row uses, but openpyxl does not write the cell formula, so the cell would stay empty until someone re-selects the function in Excel.
How do I stop pandas from reading the total row as data? Read the table's range and drop the last totalsRowCount rows, or use nrows when reading with pandas. A helper that reads a table by name and drops its totals row keeps this in one place.
Related
- Up one level: Creating Excel Tables and Autofilters with Python — tables, filters and grouping.
- Apply Autofilter Criteria and Hide the Filtered Rows with openpyxl — filters that the SUBTOTAL totals respond to.
- Add Subtotals and Grand Totals to a pandas Pivot Table — totals computed in pandas.
- Add SUM and AVERAGE Formulas with Python — plain formulas outside tables.
More on tables, filters & outlines
- How-toApply Conditional Formatting to an Excel Table That Grows
- How-toSort and Filter an Excel Online Table with Microsoft Graph
- How-toWrite an Excel Table with Formula Columns Using XlsxWriter
- How-toMake an Excel Chart Grow with Its Data
- How-toAdd an Autofilter to an Excel Sheet with openpyxl
- How-toAdd Calculated Columns to an Excel Table with Python