Skip to content

Excel tables

An Excel table gives the written range a name, an autofilter, banded rows, and one of 61 built-in styles. It is the cheapest way to make an export look deliberate.

Excel Tables with Styling

import xlsxturbo
import pandas as pd

df = pd.DataFrame({
    'Product': ['Widget A', 'Widget B', 'Widget C'],
    'Price': [19.99, 29.99, 39.99],
    'Quantity': [100, 75, 50],
})

# Create a styled Excel table with autofilter, banded rows, and auto-fit columns
xlsxturbo.df_to_xlsx(df, "report.xlsx",
    table_style="Medium9",   # Excel's default table style
    autofit=True,            # Fit column widths to content
    freeze_panes=True        # Freeze header row for scrolling
)

# Available styles: Light1-Light21, Medium1-Medium28, Dark1-Dark11
xlsxturbo.df_to_xlsx(df, "dark_table.xlsx", table_style="Dark1", autofit=True)

Named Excel Tables

Set custom names for Excel tables:

import xlsxturbo
import pandas as pd

df = pd.DataFrame({'Product': ['A', 'B'], 'Price': [10, 20]})

# Name the Excel table
xlsxturbo.df_to_xlsx(df, "report.xlsx", 
    table_style="Medium2", 
    table_name="ProductPrices"
)

# Invalid characters are auto-sanitized, digits get underscore prefix
xlsxturbo.df_to_xlsx(df, "report.xlsx",
    table_style="Medium2",
    table_name="2024 Sales Data!"  # Becomes "_2024_Sales_Data_"
)

# A name that would collide with a cell address gets the same prefix
xlsxturbo.df_to_xlsx(df, "report.xlsx",
    table_style="Medium2",
    table_name="Q1"  # Becomes "_Q1"
)

Name sanitization rules

The name you pass is adjusted to something Excel accepts. In order:

Input Result Rule
"Verka\u0308ufe" "Verkäufe" The name is normalised to NFC first
"Sales Q1!" "Sales_Q1_" Anything that is not a letter, digit or _ becomes _
"2024Sales" "_2024Sales" A leading digit gains a _ prefix
"Q1", "A1", "XFD1048576" "_Q1", "_A1", "_XFD1048576" Excel reserves names that address a cell
"R", "C", "R1C1" "_R", "_C", "_R1C1" Excel reserves the R1C1 forms and the selection shortcuts R/C
"R2D2", "C3PO", "R1_total" "_R2D2", "_C3PO", "_R1_total" Excel ignores whatever follows a complete R1C1 reference
"TRUE", "false" "_TRUE", "_false" Excel reserves its logical constants, in any case
a name over 255 characters first 255 characters Excel's length limit

The cell-reference rule is bounded by the actual grid, which ends at XFD1048576. "AAAA1" and "A1048577" address no cell, so Excel takes them as ordinary names and they pass through unchanged. A zero-padded row counts by the row it parses to: "A01" is treated as a reference (Excel offers to repair a workbook with a table of that name, exactly as for "Q1"), while "A0" — row zero — addresses nothing and passes through.

Excel stops reading an R1C1 form once it has the index and ignores the rest, so "R2D2" is the reference R2 with D2 trailing and gets the prefix. Only the leading index has to exist, which is why "R1C16385" is prefixed even though that column is past the grid. A name that never reaches an index is not a reference at all: "RCx" and "Rate1" pass through unchanged.

Normalisation happens before the character rule, and it matters because a combining mark is not a letter. Without it, "Verkäufe" typed as "Verka" plus U+0308 COMBINING DIAERESIS — which is what a lot of text on macOS looks like — would arrive as "Verka_ufe". Composing first repairs every mark that has a precomposed form. It is NFC and not NFKC, so compatibility characters stay distinct: "A1" (fullwidth A) is not folded to "A1" and does not become "_A1".

Marks with no composed form are still replaced, and Excel would have accepted them: "ไม่" becomes "ไม_" (Thai tone mark U+0E48) and "हिन्दी" becomes "हिन_दी" (Hindi virama U+094D). If you need those names exactly, pass a spelling that survives the character rule.

Workbook-level defined_names are not sanitized — a reference-shaped key there raises ConfigurationError instead, because silently renaming a defined name would leave the formulas that use it pointing at a name the workbook no longer defines.

A table name may not equal a defined name

Excel requires the two kinds of name to be unique against each other, not only within their own kind, and repairs a workbook that carries both. The name is compared after sanitization and ignoring case, and a sheet-scoped defined name collides just as a global one does:

xlsxturbo.df_to_xlsx(df, "report.xlsx",
    table_style="Medium2",
    table_name="Sales",
    defined_names={"Sheet1!Sales": "=Sheet1!$A$1:$A$4"},
)
# WorkbookValidationError: defined_names['Sheet1!Sales'] collides with the
# table name 'Sales' on sheet 'Sheet1'.

The check runs before anything is written, so the output file is left alone. A sheet that creates no table — no table_style, or an empty DataFrame — claims no name and cannot collide.

A sheet that asks for a table_style without a table_name still claims one: the writer names such tables Table1, Table2, ... numbered over the workbook's tables in sheet order, and those names take part in both checks. So defined_names={"Table1": ...} collides with the first auto-named table, and an explicit table_name="Table2" on one sheet collides with an auto-named table on the second. The message says which name was generated and where, so a name you never typed is not a mystery.