Guide
Getting Started With Python Excel AutomationDeep dive

openpyxl vs xlsxwriter vs pandas.ExcelWriter

Understand how pandas.ExcelWriter wraps the openpyxl and xlsxwriter engines, when each one wins, and write the same DataFrame three ways with a clear decision table.

The three names look like competing libraries, but they sit at different layers. pandas.ExcelWriter is not an engine at all — it's a thin wrapper that delegates to one. The real engines are openpyxl and xlsxwriter, and they have opposite strengths: openpyxl can read and write existing files and preserves styles, while xlsxwriter is write-only but faster and richer at formatting and charts. This guide, part of Writing DataFrames to Excel with Pandas, clears up the relationship, gives you a decision table, and writes the same DataFrame three ways.

openpyxl vs xlsxwriter vs pandas.ExcelWriter capabilities A matrix comparing the three across four capabilities: reading existing files, writing styles, charts, and appending sheets. openpyxl reads and appends; xlsxwriter writes styles and charts; pandas.ExcelWriter appends through openpyxl. openpyxl xlsxwriter pandas ExcelWriter Reads existing? Writes styles? Charts? Append sheets? ✓ supported – not available pandas inherits append from the openpyxl engine

Prerequisites

Install pandas and both engines:

Bash
pip install pandas openpyxl xlsxwriter

How the pieces relate

How pandas.ExcelWriter delegates to the openpyxl and xlsxwriter engines A layered flow: your to_excel or ExcelWriter call passes through the pandas.ExcelWriter wrapper, which delegates via the engine argument to either openpyxl (reads and writes, so it can edit and append) or xlsxwriter (write only, fast, charts and formats). Both send a workbook to disk, but only openpyxl also reads the existing file back. Your call df.to_excel(path) · pd.ExcelWriter(path, engine=…) pandas.ExcelWriter wrapper — picks the real engine via engine= openpyxl reads + writes edit in place · append · keep styles xlsxwriter write only fastest · rich formats · charts .xlsx workbook on disk openpyxl reads and writes the file · xlsxwriter is write-only

df.to_excel(path) and pd.ExcelWriter(path) both produce a file by handing your data to an engine. When you don't name one, pandas picks a default (openpyxl for .xlsx). You choose explicitly with engine=:

  • openpyxl — read + write. Opens an existing .xlsx, edits cells in place, appends sheets, and keeps the styles already in the file. This is the only engine that can modify a workbook you already have.
  • xlsxwriter — write only. It builds a brand-new file fast and exposes the richest formatting, conditional formats, and native charts. It cannot open or read an existing workbook.
  • pandas.ExcelWriter — the wrapper. It exposes mode="a" (append) and a book/sheets handle so you can reach the underlying engine object for formatting.

Decision table

Needopenpyxlxlsxwriterplain to_excel
Read an existing fileYesNon/a (write-only)
Edit a file in placeYesNoNo
Append a sheet (mode="a")YesNoUses openpyxl
Native chartsLimitedYes (strong)Whatever engine you pass
Conditional formattingLimitedYesVia engine
Raw write speed on large filesGoodFastestMatches the engine
Preserve styles in source fileYesn/aOnly via openpyxl
Quick one-off dumpOverkillOverkillBest fit

Write 1: plain to_excel for a quick dump

No engine argument, no formatting — the fastest way to get a DataFrame onto disk:

Python
import pandas as pd

df = pd.DataFrame({
    "Region": ["North", "South", "West"],
    "Revenue": [12000, 9800, 15400],
})
df.to_excel("dump.xlsx", index=False)   # defaults to openpyxl
print("wrote dump.xlsx")

Write 2: xlsxwriter with a cell format

Reach the workbook and worksheet objects through the ExcelWriter, then add a currency format. xlsxwriter shines here because formatting is first-class:

Python
import pandas as pd

df = pd.DataFrame({
    "Region": ["North", "South", "West"],
    "Revenue": [12000, 9800, 15400],
})

with pd.ExcelWriter("styled.xlsx", engine="xlsxwriter") as writer:
    df.to_excel(writer, sheet_name="Report", index=False)
    workbook = writer.book
    worksheet = writer.sheets["Report"]
    money = workbook.add_format({"num_format": "$#,##0", "bold": True})
    worksheet.set_column("B:B", 14, money)   # format the Revenue column

print("wrote styled.xlsx")

Write 3: openpyxl direct, to edit an existing file

This is where openpyxl is the only option: load a file that already exists, change a cell, and save. xlsxwriter physically cannot do this because it never reads:

Python
import pandas as pd
from openpyxl import load_workbook

# Start from a file written earlier
pd.DataFrame({"Region": ["North"], "Revenue": [12000]}).to_excel(
    "ledger.xlsx", index=False
)

wb = load_workbook("ledger.xlsx")   # read the existing workbook
ws = wb.active
ws["B2"].value = 13500              # edit a cell in place
ws["C1"] = "Adjusted"               # add a column header
wb.save("ledger.xlsx")

print("updated:", load_workbook("ledger.xlsx").active["B2"].value)

Appending a sheet needs openpyxl

mode="a" opens the existing file to add to it, so it requires the read-capable engine. Passing engine="xlsxwriter" with mode="a" raises a ValueError:

Python
import pandas as pd

pd.DataFrame({"Region": ["North"], "Revenue": [12000]}).to_excel(
    "book.xlsx", sheet_name="Jan", index=False
)

with pd.ExcelWriter("book.xlsx", engine="openpyxl", mode="a") as writer:
    pd.DataFrame({"Region": ["South"], "Revenue": [9800]}).to_excel(
        writer, sheet_name="Feb", index=False
    )

print(pd.ExcelFile("book.xlsx").sheet_names)

Common pitfalls

SymptomCauseFix
ValueError: Append mode is not supported with xlsxwriterxlsxwriter can't open existing filesUse engine="openpyxl" for mode="a"
FileNotFoundError/empty file when "editing"Tried to load an existing file with xlsxwriterUse load_workbook (openpyxl); xlsxwriter only creates new files
Styles vanish after a pandas round-tripReading to a DataFrame keeps values, not formattingEdit with openpyxl directly instead of read_excel then to_excel
Chart or conditional format ignoredTried to add it through the wrong engineBuild new styled reports with xlsxwriter

The styling-loss trap is the one that bites teams: pd.read_excel() returns only the data, so a read_excel → to_excel cycle silently drops every fill, border, and number format the source had. To keep formatting, never pass through a DataFrame — style the cells with openpyxl and modify them in place.

Performance and scale

For large write-only jobs, xlsxwriter is the fastest and supports constant_memory mode to stream rows without holding the whole sheet in RAM. openpyxl is competitive and adds a write_only=True mode for the same reason, but it pays a cost to preserve existing content. For a quick dump under a few thousand rows, the engine choice is irrelevant — use plain to_excel.

Picking an engine from the job

The decision is usually made by two questions: does the file already exist, and does the output need rich formatting?

Which engine suits which job Editing an existing workbook requires openpyxl because xlsxwriter cannot read. Generating a heavily formatted new file suits xlsxwriter. A quick data dump needs neither choice made explicitly. editing an existing file openpyxl only xlsxwriter cannot read styles are preserved new, heavily formatted xlsxwriter richest formatting API fastest generation a quick dump plain to_excel engine choice irrelevant keep it simple

The one combination worth remembering is append mode: pd.ExcelWriter(path, mode="a") requires openpyxl, because appending means reading the existing workbook first. Passing engine="xlsxwriter" with mode="a" raises immediately, which is the library being helpful rather than obstructive.

Formatting through the writer

Both engines expose their native objects through the writer, and the two APIs are different enough that code cannot be swapped between them:

Python
import pandas as pd

with pd.ExcelWriter("styled.xlsx", engine="xlsxwriter") as writer:
    df.to_excel(writer, sheet_name="Report", index=False)
    workbook, sheet = writer.book, writer.sheets["Report"]
    money = workbook.add_format({"num_format": "#,##0.00"})
    sheet.set_column("C:C", 14, money)          # xlsxwriter: format applies to the column

xlsxwriter attaches formats to columns and rows, which is why it is fast; openpyxl attaches them to cells, which is why it can edit an existing file. Choosing an engine therefore commits the formatting code as well as the write, and mixing the two in one project is the most common source of "why does this work on the other report?".

Pick once, per project

Because the two engines' formatting APIs are incompatible, mixing them across a codebase means every report needs its own styling helper. Choosing one as the default — openpyxl where files are edited, xlsxwriter where they are generated — and writing the finishing function against it is what makes styling reusable rather than something each report reinvents.

The wrapper is not the engine

Remembering that pandas.ExcelWriter delegates rather than implements resolves most confusion in this area: mode="a" works only because openpyxl can read, chart APIs differ because they belong to the engines, and an unsupported combination raises from the engine rather than from pandas. Reading the error message with that layering in mind usually points straight at the fix.

Presentation comes after the data

Any write replaces what it covers, so formatting, filters, images and charts belong in a single finishing pass that runs after the last value has been written. Splitting the job that way — build the frame, write it, then decorate the finished sheet — is what stops a style disappearing the month someone adds a to_excel call in the middle. It also gives a report one obvious place to change when the house style moves, instead of a dozen scattered blocks that have to be found first.

Let the job choose the engine

The engine question answers itself once the job is described: editing an existing workbook needs openpyxl, generating a large formatted file suits xlsxwriter, and a quick dump needs no decision at all. Writing that reasoning into a comment beside the ExcelWriter call saves the next reader from re-deriving it — and saves them from switching engines for a formatting feature and discovering that append mode has stopped working.

Frequently asked questions

Is pandas.ExcelWriter an engine? No. It's a context-manager wrapper that delegates to openpyxl or xlsxwriter. You pick the real engine with engine=.

Which engine does to_excel use by default? For .xlsx, pandas defaults to openpyxl when it's installed. Name engine="xlsxwriter" explicitly when you want xlsxwriter's formatting.

Can xlsxwriter edit an existing workbook? Never. It is write-only and creates a fresh file each run. Any in-place edit or append must go through openpyxl.

How do I add a chart to a pandas export? Write with engine="xlsxwriter", grab writer.book and writer.sheets[...], then build the chart with xlsxwriter's chart API.

Conclusion

Think in layers: pandas.ExcelWriter is the wrapper, openpyxl and xlsxwriter are the engines. Reach for openpyxl when you must read, edit in place, or append to a file that already exists; reach for xlsxwriter to build new, heavily styled reports and charts from scratch; and use plain to_excel for a quick dump where formatting doesn't matter. The single rule that prevents most surprises: a pandas round-trip keeps data but loses styling, so edit existing files with openpyxl directly.

Where to go next