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.

Where python-docx looks for a style name Assigning table.style triggers a lookup in the styles part of the document that was opened. A blank Document created by python-docx contains only a handful of default table styles such as Normal Table and Table Grid, so any other name raises KeyError. A document opened from a corporate template contains that template's styles, so those names resolve. Word's own style gallery is not consulted at any point. table.style = name lookup by exact name Document() library default template Normal Table, Table Grid … KeyError anything else is absent Document("house.docx") template's styles.xml whatever the template defines Style resolves house look preserved Word's ribbon gallery is never consulted — only the opened file's styles part

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.

Three ways to get the house look Three options compared. Building from a template that already defines the style is the simplest and keeps the branding exactly, but requires shipping a template file. Falling back to a built-in style such as Table Grid always works but loses the branding. Defining the style in XML needs no template file and gives full control, at the cost of writing and maintaining the style definition in code. Getting a branded table into a generated document Use a template Document("house.docx") exact branding kept no XML to maintain must ship the file Fall back "Table Grid" always available never raises branding is lost Define in XML append to styles part no template needed full control more code to maintain Default to the template; reach for XML only when shipping a binary asset is impossible

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.

Style inventory: blank document against template A blank Document created by python-docx defines two table styles, Normal Table and Table Grid. A corporate template defines those two plus Grid Table 4 Accent 1 and House Data Table. Any name outside the document's own inventory raises KeyError, which is why the same script succeeds against the template and fails against a blank document. Document() — 2 styles Document("house.docx") — 4 styles Normal Table Table Grid Normal Table Table Grid Grid Table 4 Accent 1 House Data Table anything else → KeyError

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.

Part of Building and Editing Word Tables with Python.