How-to
Getting Started With Python Excel AutomationDeep dive

Write Very Wide DataFrames to Excel

Excel stops at 16,384 columns. pandas raises "This sheet is too large!" above that — but only checks the DataFrame itself, so a frame that fits exactly loses its last column silently when the index is written. Detect the real width, then choose a fix: transpose short-and-wide data, reshape to long format with melt, or split the columns across several sheets with the key repeated on each, and check what was written.

Most DataFrames are long: many rows, a handful of columns. Some are wide. A sensor export with one column per sensor, a survey with one column per answer option, a pivot with one column per day across several years, or a feature matrix for a model can easily reach tens of thousands of columns. Excel's grid stops at column XFD, the 16,384th, and pandas refuses to write a DataFrame wider than that with ValueError: This sheet is too large!. There is also a quieter trap just below the limit: pandas checks the DataFrame's own width, not the width it will actually write, so when the index takes an extra column, a frame that "fits" loses its last column without any error. This page shows both, measures the real width before writing, and gives three ways to get wide data into a workbook that people can use. It belongs to Writing DataFrames to Excel with pandas.

Prerequisites

Bash
pip install pandas openpyxl numpy

The limits are the same for every .xlsx writer: 1,048,576 rows and 16,384 columns per sheet.

Step 1 — The error, and the silent case

Python
import numpy as np
import pandas as pd
from openpyxl import load_workbook

too_wide = pd.DataFrame(np.ones((3, 20_000)))
try:
    too_wide.to_excel("too_wide.xlsx")
except ValueError as e:
    print("ValueError:", e)

just_fits = pd.DataFrame(np.arange(3 * 16_384).reshape(3, 16_384), columns=[f"c{i}" for i in range(16_384)])
just_fits.to_excel("just_fits.xlsx")                 # index=True by default: needs 16,385 columns
ws = load_workbook("just_fits.xlsx", read_only=True).active
header = next(ws.iter_rows(min_row=1, max_row=1, values_only=True))
print("written columns:", len(header), "| last header written:", header[-1], "| last in DataFrame:", just_fits.columns[-1])

The first case fails loudly: 20,000 columns, and pandas reports the sheet size against the maximum of 1,048,576 × 16,384. The second is the one to worry about. The DataFrame has exactly 16,384 columns, so it passes pandas' check, but to_excel writes the index as well, which needs one more column than Excel has. Neither openpyxl nor XlsxWriter complains; the workbook simply ends at column XFD, and the last DataFrame column, c16383, is not in the file. The same happens with a MultiIndex, which takes one column per level, and with startcol, which shifts everything right.

Where the columns go Excel has 16,384 columns, A to XFD. A DataFrame of 16,384 columns written with its index needs 16,385, so the last DataFrame column falls beyond XFD and is silently dropped. Without the index it fits exactly. A B C D 1 Written Columns needed Fits in A–XFD? Result 2 20,000 columns 20,001 no ValueError 3 16,384 + index 16,385 no last column lost silently 4 16,384, index=False 16,384 yes complete 5 16,000 + 2-level index 16,002 yes complete

Step 2 — Measure the real width before writing

Python
MAX_COLS = 16_384

def written_width(df, index=True, startcol=0):
    return startcol + df.shape[1] + (df.index.nlevels if index else 0)

for name, df, idx in [("too_wide", too_wide, True), ("just_fits", just_fits, True), ("just_fits", just_fits, False)]:
    w = written_width(df, index=idx)
    print(f"{name:9} index={idx!s:5} needs {w:>6,} columns -> {'OK' if w <= MAX_COLS else 'TOO WIDE'}")

The width that matters is the DataFrame's columns plus one column per index level, if the index is written, plus any startcol offset. A three-line function gives that number, and a guard that raises when it exceeds 16,384 turns the silent loss into an error. Put the check next to every to_excel call that might see wide data — it costs nothing and catches the case pandas misses. The output confirms the arithmetic: the frame of exactly 16,384 columns needs 16,385 with its index, and fits only with index=False.

Step 3 — Short and wide: transpose it

Python
sensors = pd.DataFrame(np.random.default_rng(1).normal(20, 2, (3, 18_000)).round(2),
                       index=["min", "mean", "max"], columns=[f"S{i:05d}" for i in range(18_000)])
sensors.T.rename_axis("Sensor").to_excel("sensor_summary.xlsx")
print(sensors.shape, "->", sensors.T.shape)

When there are few rows and many columns — a summary of three statistics for 18,000 sensors — the data is simply lying on its side. Transposing turns 18,000 columns into 18,000 rows and three columns, well within the limits, and it is also far easier to read: a reader can sort and filter by sensor, which Excel does along rows, not columns. Naming the new index Sensor gives the first column a proper heading.

Step 4 — Wide time series: melt to long format

Python
days = pd.date_range("2000-01-01", periods=9_500, freq="D")
prices = pd.DataFrame(np.random.default_rng(2).normal(100, 5, (4, len(days))).round(2),
                      index=pd.Index(["A", "B", "C", "D"], name="Product"), columns=days)
long = prices.reset_index().melt(id_vars="Product", var_name="Date", value_name="Price")
long.to_excel("prices_long.xlsx", index=False)
print(prices.shape, "->", long.shape)
print(long.head(3).to_string(index=False))

A pivot with one column per day becomes too wide after about 45 years of daily data — or much sooner if each day has several measures. Long format, with one row per product and date, has three columns however many days there are, and up to a million rows. It is the shape pivot tables, Power Query, charts and pandas all prefer, so the reader loses nothing: a pivot table in Excel turns it back into the wide view with a few clicks. melt does the reshaping; the reverse is a pivot, covered in Unpivot a Wide Excel Sheet with pandas melt.

Transpose or melt Transpose suits short and wide data such as a few statistics for many items, turning columns into rows. Melt suits wide data with a row key, such as one column per date, turning it into one row per key and column. Transpose few rows, many columns columns become rows 3 × 18,000 → 18,000 × 3 easy to sort and filter Melt to long format one column per date or item one row per key and column 4 × 9,500 → 38,000 × 3 pivot it back in Excel

Step 5 — Keep it wide: split across sheets

Python
def write_wide(df, path, key_cols=1, chunk=16_000):
    """Split the value columns into blocks, repeating the key columns on every sheet."""
    keys, values = df.iloc[:, :key_cols], df.iloc[:, key_cols:]
    with pd.ExcelWriter(path) as xw:
        for n, start in enumerate(range(0, values.shape[1], chunk), start=1):
            block = pd.concat([keys, values.iloc[:, start:start + chunk]], axis=1)
            assert written_width(block, index=False) <= MAX_COLS
            block.to_excel(xw, sheet_name=f"Cols {start + 1}-{start + block.shape[1] - key_cols}", index=False)
    return n

matrix = pd.concat([pd.Series(["r1", "r2"], name="Row"),
                    pd.DataFrame(np.ones((2, 40_000), dtype=int), columns=[f"f{i}" for i in range(40_000)])], axis=1)
sheets = write_wide(matrix, "feature_matrix.xlsx")
with pd.ExcelFile("feature_matrix.xlsx") as book:
    print(sheets, "sheets:", book.sheet_names)
    print(sum(book.parse(s).shape[1] - 1 for s in book.sheet_names), "value columns written of", matrix.shape[1] - 1)

Sometimes the wide layout is the point — a system on the other end expects it, or the reader wants to scroll across. Then split the columns into blocks of at most 16,000, and repeat the key columns at the start of every sheet so each sheet can be read on its own. Naming the sheets after the column range they hold makes it easy to find a given column. The check at the end counts the value columns across all sheets and compares with the source, the kind of reconciliation that turns "it probably wrote everything" into a fact. Be honest with the reader, though: 40,000 columns across three sheets is hard to use in Excel, and if the consumer is another program, CSV or Parquet is usually the better format — see Excel vs CSV vs Parquet for Python Data Pipelines.

Why wide sheets are hard to use anyway

Even below the limit, very wide sheets work against the way Excel is built. Sorting, filtering, tables and pivot tables all treat a row as a record and a column as a field, so data with thousands of fields per record leaves most of those tools with nothing to do. Scrolling across thousands of columns is slow, column letters such as AKZ mean nothing to a reader, frozen panes can only keep a few columns in view, and printing is out of the question. File size and opening time grow too, because every cell is stored separately whatever its shape. A useful rule of thumb is that if a sheet is wider than it is long, it is probably on its side. Choosing the shape for the reader, rather than the shape the data happened to arrive in, solves the limit and the usability problem at the same time.

Choosing a fix for a wide DataFrame First compute the written width including the index. If it fits, write it. If not, transpose short-and-wide data, melt data that has a row key and one column per date or item, and split across sheets only when the wide layout itself is required. Written width columns + index levels ≤ 16,384? write as is Few rows? transpose Key + one column per date? melt to long Wide layout required? split across sheets

Common pitfalls

  • Trusting pandas' check. It ignores the index; measure the written width yourself.
  • MultiIndex and startcol. Each adds columns to the written width.
  • Keeping data on its side. Short-and-wide data reads better transposed.
  • Daily columns for years. Melt to long format and let pivot tables widen it.
  • Splitting without keys. Repeat the key columns on every sheet so each stands alone.

Conclusion

Excel sheets end at 16,384 columns. pandas raises an error for DataFrames wider than that, but it does not count the index, so a frame that fits exactly loses its last column silently when written with index=True. Compute the written width — columns, plus index levels, plus startcol — before writing, then transpose short-and-wide data, melt time series to long format, or split the columns across sheets with the key columns repeated, and count the columns written afterwards.

Frequently asked questions

What is the maximum number of columns in Excel? 16,384 per sheet, up to column XFD, with 1,048,576 rows.

What does "This sheet is too large! Your sheet size is …" mean in pandas? The DataFrame is wider or longer than an Excel sheet allows; reshape it, split it across sheets or use another format.

Why is the last column missing after to_excel? If the DataFrame has exactly 16,384 columns and the index is written, the index takes one column and the last data column falls beyond XFD; use index=False or reshape.

How do I write a DataFrame with more than 16,384 columns to Excel? Transpose it if it is short, melt it to long format if it has a key, or split its columns across several sheets.

More on writing excel files

More on writing excel files →