Choose and Customise Excel Table Styles with Python
An Excel table brings filter buttons, structured references and a growing range — and a style that colours the header, stripes the rows and can bold the first and last columns. From Python the style is a single name plus four switches, which makes it easy to pick one, easy to mistype one, and easy to miss that some applications ignore table styles altogether. This page goes through the built-in style families and the switches, sets them from openpyxl and XlsxWriter, writes a custom company table style, adds a check for style names, and shows how to make a table look right when the workbook is opened in LibreOffice or converted to PDF. Creating the table itself is covered in Create an Excel Table with Python; this page is about how it looks. It belongs to Creating Excel Tables and Autofilters with Python.
Prerequisites
pip install openpyxl xlsxwriter pillow
The examples use a small regional sales table with a header row and four data rows (rows in the code). Step 6 uses LibreOffice (soffice) and Pillow to render the result and look at its colours; that part is optional.
Step 1 — How the built-in styles are named
Excel ships 60 table styles in three families, and every one has an internal name of the form TableStyle<Family><Number>:
from collections import Counter
from openpyxl.worksheet.table import TABLESTYLES
families = Counter(name.rstrip("0123456789").replace("TableStyle", "") for name in TABLESTYLES)
print(dict(families))
print(TABLESTYLES[0], "…", TABLESTYLES[-1])
There are 21 Light styles, 28 Medium and 11 Dark. Within each family the numbers run in groups of seven colours in the order of the default Office theme: the first style of each group is neutral (black or grey), and the next six use the theme's accent colours (the last four Dark styles break the pattern with two-colour combinations). TableStyleMedium9 — the blue banded style Excel picks by default — is the second style of the second Medium group. Because the colours come from the workbook's theme, the same style name looks different in a workbook with a different theme. Excel shows the styles in a gallery under Table Design → Table Styles, which is the quickest way to choose one: hover to preview, then note the name from its tooltip ("Medium 9" means TableStyleMedium9).
Step 2 — The four switches
A style defines formats for several table elements — header row, total row, first and last column, row stripes, column stripes — and four switches decide which of them apply:
from openpyxl import Workbook
from openpyxl.worksheet.table import Table, TableStyleInfo
wb = Workbook()
ws = wb.active
ws.title = "Sales"
for r in rows:
ws.append(r)
ws.add_table(Table(displayName="Sales", ref="A1:D5", tableStyleInfo=TableStyleInfo(
name="TableStyleMedium2",
showRowStripes=True, # alternate row shading
showColumnStripes=False, # alternate column shading
showFirstColumn=True, # emphasise the label column
showLastColumn=True, # emphasise the total column
)))
wb.save("table_switches.xlsx")
print(ws.tables["Sales"].tableStyleInfo.name, "first/last column on")
For a report with labels in the first column and a total in the last, turning on first and last column emphasis makes both stand out; for a wide table of similar measures, column stripes help the eye down each column. Row stripes are the usual default. The header row itself is part of the table definition (headerRowCount), not a switch, and the total row is controlled by totalsRowCount — adding one is covered in Add a Total Row to an Excel Table.
Step 3 — The same in XlsxWriter
XlsxWriter writes the same table XML but uses Excel's display names and lower-case switch names:
import xlsxwriter
book = xlsxwriter.Workbook("table_xlsxwriter.xlsx")
sheet = book.add_worksheet("Sales")
sheet.add_table("A1:D5", {
"name": "Sales",
"style": "Table Style Light 11",
"banded_rows": False,
"banded_columns": True,
"first_column": True,
"last_column": True,
"columns": [{"header": h} for h in rows[0]],
"data": rows[1:],
})
plain = book.add_worksheet("Plain")
plain.add_table("A1:D5", {"name": "PlainSales", "style": None,
"columns": [{"header": h} for h in rows[0]], "data": rows[1:]})
book.close()
print("written with Table Style Light 11 and with no style")
"Table Style Light 11" is written to the file as TableStyleLight11. "style": None gives a table with filter buttons and structured references but no colours at all — useful when you want to apply your own cell formatting. XlsxWriter has no API for custom table styles; for those, use openpyxl as in the next step.
Step 4 — A custom company table style
A custom table style is a named set of differential formats (the same partial formats conditional formatting uses), one per table element. openpyxl can write one, though only through private attributes of the workbook — _differential_styles and _table_styles — so pin your openpyxl version and keep this code in one function:
from openpyxl.styles import Border, Font, PatternFill, Side
from openpyxl.styles.differential import DifferentialStyle
from openpyxl.styles.table import TableStyle, TableStyleElement
def add_table_style(wb, name, elements):
"""elements: {"headerRow": DifferentialStyle, ...}; returns the style name."""
parts = []
for kind, dxf in elements.items():
wb._differential_styles.append(dxf)
parts.append(TableStyleElement(type=kind, dxfId=len(wb._differential_styles.dxf) - 1))
wb._table_styles.tableStyle.append(TableStyle(name=name, pivot=False, count=len(parts), tableStyleElement=parts))
return name
wb = Workbook()
ws = wb.active
ws.title = "Sales"
for r in rows:
ws.append(r)
brand = add_table_style(wb, "BrandTable", {
"wholeTable": DifferentialStyle(border=Border(bottom=Side("thin", color="C7D2FE"))),
"headerRow": DifferentialStyle(font=Font(bold=True, color="FFFFFF"), fill=PatternFill("solid", bgColor="4338CA")),
"firstRowStripe": DifferentialStyle(fill=PatternFill("solid", bgColor="EEF2FF")),
"lastColumn": DifferentialStyle(font=Font(bold=True)),
})
ws.add_table(Table(displayName="Sales", ref="A1:D5",
tableStyleInfo=TableStyleInfo(name=brand, showRowStripes=True, showLastColumn=True)))
wb.save("table_custom.xlsx")
from openpyxl import load_workbook
back = load_workbook("table_custom.xlsx")
print([s.name for s in back._table_styles.tableStyle], "→", back["Sales"].tables["Sales"].tableStyleInfo.name)
The workbook's styles.xml now holds a BrandTable style with four elements, and the table refers to it by name; reloading the file shows the style survives a round trip through openpyxl. In Excel it appears under Custom at the top of the table style gallery, so colleagues can apply it to their own tables. Note that in differential formats a solid fill's colour goes in bgColor, not fgColor — the same rule as for conditional formatting in Highlight Cells Above a Threshold with openpyxl.
Step 5 — Catch misspelt style names
openpyxl and Excel accept any string as a style name. A typo such as TableStyleMedium29 (there are only 28) is written without complaint, and the table then appears unstyled. A small check before saving catches it:
def check_table_styles(wb):
known = set(TABLESTYLES) | {s.name for s in wb._table_styles.tableStyle}
problems = []
for sheet in wb.worksheets:
for table in sheet.tables.values():
name = table.tableStyleInfo.name if table.tableStyleInfo else None
if name is not None and name not in known:
problems.append(f"{sheet.title}!{table.displayName}: unknown style {name!r}")
return problems
ws.tables["Sales"].tableStyleInfo.name = "TableStyleMedium29"
print(check_table_styles(wb))
ws.tables["Sales"].tableStyleInfo.name = brand
print(check_table_styles(wb))
The check accepts the 60 built-in names plus any custom styles defined in the workbook, and reports the sheet and table of anything else. Running it in a test of the report generator stops a silently plain table reaching readers.
Step 6 — Tables in LibreOffice and PDF exports
Table styles are drawn by Excel. LibreOffice (tested with version 26.2) keeps the table's range and filter buttons but does not draw table styles — neither built-in ones like Medium9 nor custom ones — so the same file opened there, or converted to PDF with LibreOffice on a server, shows a plain grid. If that matters, add the look as ordinary cell formatting on top of the table. Excel lets direct cell formatting take precedence over the table style, and LibreOffice draws it as normal:
from openpyxl.styles import Font, PatternFill
def bake_table_look(ws, table, header="4338CA", stripe="EEF2FF"):
"""Copy a simple header + row-stripe look into direct cell formatting."""
(first, *body) = ws[table.ref]
for cell in first:
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = PatternFill("solid", fgColor=header)
for i, row in enumerate(body):
if i % 2 == 0:
for cell in row:
cell.fill = PatternFill("solid", fgColor=stripe)
bake_table_look(ws, ws.tables["Sales"])
wb.save("table_baked.xlsx")
print("header and stripes copied into", ws.tables["Sales"].ref)
Note that ordinary cell fills use fgColor — it is only differential formats that use bgColor. Rendering both files shows the difference:
import subprocess
from PIL import Image
def colour_share(xlsx, rgb):
subprocess.run(["soffice", "--headless", "--convert-to", "png", xlsx], check=True, capture_output=True)
img = Image.open(xlsx.replace(".xlsx", ".png")).convert("RGB")
counts = img.getcolors(img.width * img.height)
near = sum(n for n, p in counts if all(abs(a - b) < 12 for a, b in zip(p, rgb)))
return near / (img.width * img.height)
indigo = (0x43, 0x38, 0xCA)
for f in ("table_custom.xlsx", "table_baked.xlsx"):
print(f"{f:18} indigo header pixels: {colour_share(f, indigo):.2%}")
The custom-style file renders with no indigo at all in LibreOffice, while the baked file shows the indigo header. The baked formatting is fixed to the rows it was written to — it does not move when rows are sorted or added in Excel the way a table style does — so use it for finished reports and rely on the table style for workbooks people will keep editing. Striping a plain range without a table is covered in Add Alternating Row Colours with openpyxl.
Common pitfalls
- A misspelt style name. It is written without error and the table appears plain; validate names.
- Expecting the same colours in every workbook. Built-in styles use theme colours.
fgColorin a table style. Differential formats take a solid fill's colour frombgColor.- Relying on table styles in LibreOffice or PDF exports. They are not drawn; bake the look into cells.
- Baked formatting in a table people sort. Direct fills stay on their rows; keep the table style for live workbooks.
Conclusion
An Excel table's look is a style name from the Light, Medium or Dark families plus four switches for row stripes, column stripes and first and last column emphasis. openpyxl sets them through TableStyleInfo, XlsxWriter through style, banded_rows and friends, and openpyxl can also define a custom company style from differential formats. Validate style names, since typos pass silently, and when a workbook will be opened in LibreOffice or converted to PDF, copy the look into direct cell formatting because table styles are only drawn by Excel.
Frequently asked questions
How do I set the style of an Excel table in openpyxl? Pass a TableStyleInfo to the Table with a name such as TableStyleMedium9 and the switches showRowStripes, showColumnStripes, showFirstColumn and showLastColumn.
What table style names does Excel have? Sixty built-in styles: TableStyleLight1 to Light21, TableStyleMedium1 to Medium28 and TableStyleDark1 to Dark11. openpyxl lists them in the TABLESTYLES tuple of its worksheet.table module.
Can I create a custom table style with Python? Yes, with openpyxl, by adding differential formats and a TableStyle to the workbook through its private _differential_styles and _table_styles attributes. XlsxWriter does not support custom table styles.
Why is my Excel table unstyled in LibreOffice? LibreOffice does not draw Excel table styles. Copy the header and stripe formatting into the cells directly if the file will be opened there or converted to PDF with it.
Related
- Up one level: Creating Excel Tables and Autofilters with Python — tables, filters and outlines.
- Create an Excel Table with Python — defining the table itself.
- Convert an Excel Table Back to a Normal Range — removing a table but keeping its look.
- Change the Default Font of an Excel Workbook with Python — workbook-wide styling.
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 a Total Row to an Excel Table with Python
- How-toAdd an Autofilter to an Excel Sheet with openpyxl