pola-rs/polars · error · DuplicateError

cannot create a second {col!r} column

Error message

cannot create a second {col!r} column

What it means

polars.exceptions.DuplicateError raised while preparing an Excel write (write_excel) from _xl_inject_dummy_table_columns. Sparklines, column_formulas, and row_totals are realized by injecting placeholder columns into the frame; if one of those definitions uses a column name that already exists in the DataFrame, injecting it would create a duplicate column, which polars forbids.

Source

Thrown at py-polars/src/polars/io/spreadsheet/_write_utils.py:247

    )


def _xl_inject_dummy_table_columns(
    df: DataFrame,
    coldefs: dict[str, Any],
    *,
    dtype: dict[str, PolarsDataType] | PolarsDataType | None = None,
    expr: Expr | None = None,
) -> DataFrame:
    """Insert dummy frame columns in order to create empty/named table columns."""
    df_original_columns = set(df.columns)
    df_select_cols = df.columns.copy()
    cast_lookup = {}

    for col, definition in coldefs.items():
        if col in df_original_columns:
            msg = f"cannot create a second {col!r} column"
            raise DuplicateError(msg)
        elif not isinstance(definition, dict):
            df_select_cols.append(col)
        else:
            cast_lookup[col] = definition.get("return_dtype")
            insert_before = definition.get("insert_before")
            insert_after = definition.get("insert_after")

            if insert_after is None and insert_before is None:
                df_select_cols.append(col)
            else:
                insert_idx = (
                    df_select_cols.index(insert_after) + 1  # type: ignore[arg-type]
                    if insert_before is None
                    else df_select_cols.index(insert_before)
                )
                df_select_cols.insert(insert_idx, col)

    expr = F.lit(None) if expr is None else expr

View on GitHub (pinned to df599052da)

Solutions

  1. Give the injected column a new, unused name (e.g. 'trend', 'total_x').
  2. Or drop/rename the existing DataFrame column before writing if the sheet column should be formula/sparkline-driven instead of data-driven.
  3. Assert upfront that set(sparklines) | set(column_formulas) | set(row_totals or []) is disjoint from set(df.columns).

Example fix

# before
pl.write_excel(df, sparklines={'total': {'columns': ['q1', 'q2', 'q3', 'q4']}})  # 'total' already in df

# after
pl.write_excel(df, sparklines={'trend': {'columns': ['q1', 'q2', 'q3', 'q4']}})
Defensive patterns

Strategy: validation

Validate before calling

injected = set(sparklines or {}) | set(column_formulas or {})
if row_totals:
    injected |= {row_totals} if isinstance(row_totals, str) else set(row_totals or [])
clash = injected & set(df.columns)
if clash:
    raise ValueError(f'write_excel placeholder names collide with df columns: {sorted(clash)}')
pl.write_excel(df, sparklines=sparklines, column_formulas=column_formulas, row_totals=row_totals)

Try / catch

try:
    pl.write_excel(df, sparklines=sparklines)
except pl.exceptions.DuplicateError as e:
    if 'cannot create a second' in str(e):
        # rename the colliding placeholder and retry
        renamed = {f'{k}_x': v for k, v in sparklines.items() if k in df.columns}
        pl.write_excel(df, sparklines={**{k: v for k, v in sparklines.items() if k not in df.columns}, **renamed})
    else:
        raise

Prevention

When it happens

Trigger: pl.write_excel(df, sparklines={'total': {'columns': ['q1','q2']}}) when 'total' is already a df column; same for column_formulas={'total': {...}} or row_totals named after an existing column.

Common situations: Writing a report whose DataFrame already contains an aggregate column and trying to overlay an in-sheet sparkline/formula with the same header; schemas that evolved (a computed 'total' column was added upstream) breaking a previously working write_excel call.

Related errors


AI-assisted analysis of pola-rs/polars@df599052da (2026-08-16). Data as JSON: /api/errors/9cae133237bf9f15. Report an issue: GitHub.