Fix python-docx Table Style Not Found
Assigning a table style stops the script:
KeyError: "no style with name 'Light Grid Accent 1'"
The same code works on a colleague's machine, or worked last week against a different input file. Nothing about the library changed — the document changed, and table styles are a property of the document, not of python-docx or of Word.
Root Cause
A .docx file carries its own styles.xml. Word's style gallery shows dozens of table styles, but a document only contains the ones it has used or that its template defined. python-docx looks up the name in the document you opened, so a blank Document() — which starts from the library's minimal default template — offers a short list, and anything outside it raises KeyError.
Two further wrinkles produce the same error with a correct-looking name. Style names are localised: a document created in a French Word installation stores Grille du tableau, not Table Grid. And the display name in Word's ribbon is not always the stored name — Word shows Grid Table 4 - Accent 1 while the file stores Grid Table 4 Accent 1, without the dash.
Minimal Diagnostic
Ask the document what it has, rather than guessing at names.
# pip install "python-docx>=1.1,<2"
from pathlib import Path
from docx import Document
from docx.enum.style import WD_STYLE_TYPE
DOCX = Path("data/report_template.docx")
def list_table_styles(docx_path: Path | None = None) -> list[str]:
"""Names of every table style the document actually defines."""
if docx_path is not None and not docx_path.exists():
raise FileNotFoundError(f"No such document: {docx_path}")
try:
document = Document(str(docx_path)) if docx_path else Document()
except Exception as exc:
raise RuntimeError(f"Could not open {docx_path}: {exc}") from exc
return sorted(s.name for s in document.styles if s.type == WD_STYLE_TYPE.TABLE)
if __name__ == "__main__":
print("blank document:", list_table_styles())
print("template:", list_table_styles(DOCX))
blank document: ['Normal Table', 'Table Grid']
template: ['Grid Table 4 Accent 1', 'House Data Table', 'Normal Table', 'Table Grid']
That single comparison answers the question in one run: the name you want exists in the template and not in a blank document, so the fix is to start from the template.
The Fix: Start From a Template That Defines the Style
# pip install "python-docx>=1.1,<2" "pandas>=2.2,<3"
from pathlib import Path
import pandas as pd
from docx import Document
TEMPLATE = Path("data/house_template.docx")
OUT = Path("out/report.docx")
def build_from_template(frame: pd.DataFrame, template: Path, dest: Path,
style: str = "House Data Table") -> Path:
"""Create a document from a styled template so the style name resolves."""
if not template.exists():
raise FileNotFoundError(f"Template not found: {template}")
document = Document(str(template)) # inherits styles.xml from the template
available = {s.name for s in document.styles}
if style not in available:
raise KeyError(
f"{style!r} is not defined in {template.name}. "
f"Available table styles: {sorted(n for n in available if 'Table' in n)}"
)
table = document.add_table(rows=len(frame) + 1, cols=len(frame.columns))
table.style = style
for position, name in enumerate(frame.columns):
table.cell(0, position).text = str(name)
for row_index, record in enumerate(frame.itertuples(index=False), start=1):
for position, value in enumerate(record):
table.cell(row_index, position).text = "" if pd.isna(value) else str(value)
dest.parent.mkdir(parents=True, exist_ok=True)
document.save(str(dest))
return dest
if __name__ == "__main__":
df = pd.DataFrame({"Region": ["EMEA", "AMER"], "Units": [1204, 980]})
print(build_from_template(df, TEMPLATE, OUT))
Raising a KeyError with the available names in the message is worth the four extra lines. The library's own error says only that the style is missing; listing what is there turns a support ticket into a one-line fix.
Producing the template is a manual step, done once: open Word, insert a table, apply the style you want, delete the table, save the file as house_template.docx. Deleting the table does not remove the style definition, so the file carries the style with no content.
Variant Fix 1: Fall Back Instead of Failing
Batch jobs that accept templates from several teams should degrade rather than stop.
# pip install "python-docx>=1.1,<2"
def pick_style(document, preferred: list[str], default: str = "Table Grid") -> str:
"""First available style from a preference list, else a guaranteed default."""
available = {s.name for s in document.styles}
for name in preferred:
if name in available:
return name
if default not in available:
raise KeyError(f"Neither {preferred} nor the default {default!r} exist in this document")
return default
if __name__ == "__main__":
style = pick_style(document, ["House Data Table", "Grid Table 4 Accent 1", "Light List"])
table.style = style
print(f"using table style {style!r}")
Log which style was chosen. A silent fallback that produces an unbranded document is the failure mode this is meant to avoid, and the log line is what makes it visible before the pack reaches a client.
Variant Fix 2: Localised Style Names
Documents created in a non-English Word store localised names. The English name will not resolve, and the localised one may contain characters that are easy to mistype.
# pip install "python-docx>=1.1,<2"
LOCALISED = {
"Table Grid": ["Table Grid", "Grille du tableau", "Tabellenraster", "Cuadrícula de tabla"],
"Normal Table": ["Normal Table", "Tableau Normal", "Normale Tabelle"],
}
def resolve_localised(document, canonical: str) -> str:
"""Find whichever localised spelling of a built-in style this document uses."""
available = {s.name for s in document.styles}
for candidate in LOCALISED.get(canonical, [canonical]):
if candidate in available:
return candidate
raise KeyError(f"No localised variant of {canonical!r} found; document has {sorted(available)[:8]}")
The style identifier in the XML (w:styleId) is not localised, which is why a document round-trips correctly between Word installations even though the display names differ. python-docx matches on the display name, hence the mapping above.
Variant Fix 3: Style the Table Directly
When neither a template nor a matching style is available, apply the formatting yourself. It is more code, but it is deterministic and travels with the script.
# pip install "python-docx>=1.1,<2"
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Pt
def shade(cell, hex_colour: str) -> None:
"""Solid background fill on one cell."""
shading = OxmlElement("w:shd")
shading.set(qn("w:val"), "clear")
shading.set(qn("w:color"), "auto")
shading.set(qn("w:fill"), hex_colour)
cell._tc.get_or_add_tcPr().append(shading)
def style_manually(table, header_fill: str = "1F3864", band_fill: str = "EFF6FF") -> None:
"""Bold white header on a dark fill, with banded body rows."""
table.style = "Table Grid" # borders come free from the built-in
for cell in table.rows[0].cells:
shade(cell, header_fill)
for run in cell.paragraphs[0].runs or [cell.paragraphs[0].add_run("")]:
run.bold = True
run.font.size = Pt(10)
run.font.color.rgb = None # inherit; set explicitly for white text
for index, row in enumerate(table.rows[1:], start=1):
if index % 2 == 0:
for cell in row.cells:
shade(cell, band_fill)
This is also the route to take when the requirement is a specific look rather than a named corporate style — the same direct-formatting approach used for text in set fonts and styles with python-docx.
Verification
Assert the style actually landed. Assignment succeeding is not proof: a fallback may have run.
# pip install "python-docx>=1.1,<2"
from pathlib import Path
from docx import Document
def assert_table_style(docx_path: Path, expected: str, table_index: int = 0) -> None:
"""Reopen the saved document and confirm the table carries the expected style."""
document = Document(str(docx_path))
assert len(document.tables) > table_index, f"no table at index {table_index}"
table = document.tables[table_index]
actual = table.style.name if table.style else None
assert actual == expected, f"table style is {actual!r}, expected {expected!r}"
defined = {s.name for s in document.styles}
assert expected in defined, f"{expected!r} is used but not defined in the saved file"
print(f"{docx_path.name}: table style {actual!r} verified")
if __name__ == "__main__":
assert_table_style(Path("out/report.docx"), "House Data Table")
The second assertion catches a subtle packaging bug: a style referenced but not defined produces a document Word opens with the table unstyled and no warning. Checking both the reference and the definition is what makes the test meaningful — and worth running in the same job that builds the pack, as described in scheduling and logging automation jobs.
FAQ
Why does Table Grid always work?
It is one of the few table styles python-docx's default template defines, so it exists in every document the library creates. Treat it as the guaranteed fallback rather than as a design choice.
Can I copy a style from one document into another in code? Yes, by deep-copying the style element from the source document's styles part into the target's. It works, but shipping the template file is simpler and far less brittle than element surgery across two packages.
Why does the table look unstyled even though no error was raised? Either a fallback style was applied, or the style is referenced without being defined. The verification above distinguishes the two.
Does docxtpl have the same problem?
No — templates rendered with docxtpl start from a real .docx, so every style in that file is available. Style errors there usually come from the rendered content losing direct formatting instead, which is covered in fix docxtpl formatting lost in output.
Are style names case-sensitive?
Yes. table grid raises the same KeyError as a genuinely absent style, which is worth checking before assuming the document is at fault.
Related
- Building and Editing Word Tables with Python — the table workflow this error interrupts
- Merge Table Cells with python-docx — layout work that assumes a working style
- Set Fonts and Styles with python-docx — direct formatting when no style exists
- Automating Word Document Creation — templates and document structure
- Dynamic Mail Merge with Python — rendering into a styled template