Fix
Getting Started With Python Excel AutomationDeep dive

Fix Text Truncated at 32,767 Characters in Excel Output

An Excel cell holds at most 32,767 characters, and Python writers enforce it differently: openpyxl cuts long text silently, xlsxwriter returns -2, pandas warns. Find the long values before writing, make truncation an error, and keep the full text by splitting it across cells, linking to a file, or choosing another format.

A column of comments, JSON payloads or log excerpts is exported to Excel, and some values come back shorter than they went in. If the export uses pandas you may see this, once, among other output:

Text
UserWarning: Cell contents too long (40000), truncated to 32767 characters

With openpyxl or xlsxwriter used directly you usually see nothing at all. An Excel cell can hold at most 32,767 characters; every writer enforces that limit, but only pandas mentions it. This page shows what each library does, how to find the long values before they are cut, and three ways to keep the full text. It is part of Troubleshooting Common Python Excel Errors.

Prerequisites

Bash
pip install pandas openpyxl xlsxwriter

tickets.pkl holds five support tickets with a free-text Notes column; three of the notes are long conversation histories, as happens when a system appends every message to one field.

Step 1 — Find the values that will not fit

Measure before writing. The check is cheap and tells you exactly which records and columns are affected:

Python
LIMIT = 32_767

tickets = pd.read_pickle("tickets.pkl")
lengths = tickets.select_dtypes(include=["object", "string"]).apply(lambda col: col.str.len())
too_long = lengths[lengths.gt(LIMIT).any(axis=1)]
print(lengths.max().to_string())
print(too_long.to_string())

Three of the five tickets exceed the limit, the longest by more than double: 74,721 characters. Knowing the size of the overshoot matters for the fix: a few values slightly over is a different problem from a column that is routinely ten times too long.

Long values rarely come straight from a source system. More often the pipeline builds them: a groupby that joins every note per customer into one cell, a column of JSON payloads, an HTML email body, a stack trace pasted into a log field. Aggregations are the usual culprit, because they grow with the data. A summary that was fine in January exceeds the limit by June:

Python
per_status = tickets.groupby("Status")["Notes"].agg(" | ".join)
print(per_status.str.len().to_string())

Both joined cells are two to three times over the limit. If a report needs "all notes per group", that is a sign the notes belong on their own sheet, one row per note, with the group as a column to filter on, rather than concatenated into one cell.

Step 2 — What each writer does

Write the same DataFrame three ways and measure what is actually in the files:

Python
import warnings
import xlsxwriter
from openpyxl import Workbook, load_workbook

long_note = tickets.loc[tickets["Ticket"] == "T-9005", "Notes"].item()

wb = Workbook()
wb.active["A1"] = long_note
wb.save("openpyxl.xlsx")

wbx = xlsxwriter.Workbook("xlsxwriter.xlsx")
status = wbx.add_worksheet().write_string("A1", long_note)
wbx.close()

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    tickets.to_excel("pandas.xlsx", index=False)

print("original:", len(long_note))
print("openpyxl:", len(load_workbook("openpyxl.xlsx").active["A1"].value), "(no warning)")
print("xlsxwriter:", len(load_workbook("xlsxwriter.xlsx").active["A1"].value), "return value", status)
print("pandas:", len(load_workbook("pandas.xlsx").active["C6"].value), "warnings:", len(caught))

All three store exactly 32,767 characters. openpyxl cuts the string when it is assigned to the cell and says nothing. xlsxwriter's write_string returns -2, which code rarely checks. pandas emits one UserWarning per long cell, which scrolls past in a scheduled job's log.

How each writer handles a 74,721-character value A table of three writers. openpyxl truncates to 32,767 characters silently when the value is assigned. xlsxwriter truncates and returns minus two from write_string. pandas truncates and emits a UserWarning. None raises an error. Writer Stored Tells you openpyxl 32,767 nothing xlsxwriter 32,767 returns -2 pandas to_excel 32,767 UserWarning

Step 3 — Make truncation an error

Losing data silently is the real bug. In pandas, turn that specific warning into an exception for the duration of the export:

Python
def to_excel_strict(frame, path, **kwargs):
    with warnings.catch_warnings():
        warnings.filterwarnings("error", message="Cell contents too long")
        frame.to_excel(path, **kwargs)

try:
    to_excel_strict(tickets, "strict.xlsx", index=False)
except UserWarning as e:
    print("stopped:", e)

With openpyxl, check lengths before assigning, as in Step 1; the cut happens on assignment, so there is nothing to check afterwards. With xlsxwriter, wrap write_string so a -2 becomes an exception that names the cell:

Python
def write_text(ws, row, col, text):
    if ws.write_string(row, col, text) == -2:
        raise ValueError(f"text in row {row + 1}, column {col + 1} is {len(text):,} characters; the limit is {LIMIT:,}")

wbx = xlsxwriter.Workbook("checked.xlsx")
sheet = wbx.add_worksheet()
try:
    write_text(sheet, 4, 2, long_note)
except ValueError as e:
    print(e)
finally:
    wbx.close()

Failing is the right default for data exports; the next steps are the ways to succeed without losing text.

Fix 1 — Split long text across continuation cells

If the text must live in the workbook, split it into chunks of at most 32,767 characters, each in its own cell to the right, and mark how many parts there are. Split on whitespace near the boundary so words stay whole:

Python
def chunks(text, size=LIMIT):
    parts = []
    while len(text) > size:
        cut = text.rfind(" ", 0, size)
        cut = size if cut <= 0 else cut + 1
        parts.append(text[:cut])
        text = text[cut:]
    parts.append(text)
    return parts

split = tickets.copy()
parts = split["Notes"].map(chunks)
width = parts.map(len).max()
for i in range(width):
    split[f"Notes part {i + 1}"] = parts.map(lambda p: p[i] if i < len(p) else "")
split["Notes parts"] = parts.map(len)
split = split.drop(columns="Notes")
to_excel_strict(split, "split.xlsx", index=False)
print(split[["Ticket", "Notes parts"]].to_string(index=False))

Reading it back, join the parts in order and compare with the original:

Python
back = pd.read_excel("split.xlsx", keep_default_na=False)
part_cols = [c for c in back.columns if c.startswith("Notes part ")]
back["Notes"] = back[part_cols].apply(lambda r: "".join(r), axis=1)
print("round trip exact:", back["Notes"].tolist() == tickets["Notes"].tolist())

The parts column records how many cells belong together, so the reader does not have to guess, and the note in the header row, "Notes part 1", tells anyone opening the file in Excel that the text continues to the right. Put the parts on the far right of the sheet, after the columns people filter and sort on, so the long text does not push the useful columns out of view.

keep_default_na=False matters: without it, empty part cells come back as NaN, and joining fails or inserts the text "nan".

One long note becomes three cells On the left a ticket whose notes are 74,721 characters in a single value. On the right the same ticket with Notes part 1, part 2 and part 3, each under the limit, and a column recording that there are three parts. Ticket Notes T-9005 74,721 characters Ticket Part 1 Part 2 Part 3 Parts T-9005 ≤32,767 ≤32,767 rest 3 before after chunks()

Fix 2 — Keep the full text outside the workbook

Nobody reads 40,000 characters in a spreadsheet cell. Often the better report shows a preview and links to the full text, written alongside the workbook:

Python
from pathlib import Path

notes_dir = Path("notes")
notes_dir.mkdir(exist_ok=True)
preview = tickets[["Ticket", "Status"]].copy()
preview["Notes (first 200 chars)"] = tickets["Notes"].str.slice(0, 200)
preview["Full notes"] = ""
with pd.ExcelWriter("tickets_with_links.xlsx", engine="xlsxwriter") as writer:
    preview.to_excel(writer, index=False, sheet_name="Tickets")
    ws = writer.sheets["Tickets"]
    for row, (ticket, text) in enumerate(zip(tickets["Ticket"], tickets["Notes"]), start=1):
        path = notes_dir / f"{ticket}.txt"
        path.write_text(text, encoding="utf-8")
        ws.write_url(row, 3, f"external:{path.as_posix()}", string=f"{len(text):,} characters")
print(sorted(p.name for p in notes_dir.iterdir()))

The external: prefix makes xlsxwriter write a relative file link, which keeps working when the workbook and the notes folder are copied or zipped together.

Where should the long text go? A decision diagram. If people need the full text inside the workbook, split it into continuation cells. If people only need to see that it exists and open it on demand, show a preview and link to a text file. If a program reads the export, use CSV, JSON Lines or Parquet instead of Excel. Who reads the long text? people, in the workbook Split into parts Fix 1 people, on demand Preview + link Fix 2 a program Another format Fix 3

A preview of the first 200 characters is usually what people scan for anyway: the opening line of a ticket, the error message at the top of a log. Put the length next to it, as the link text does here, so a reader can see at a glance which records are unusually long, which is often a finding in itself: a ticket with 75,000 characters of notes has a story behind it.

Fix 3 — Use a format without the limit

If the consumer is another program rather than a person, Excel is the wrong container for long text. CSV, JSON Lines and Parquet have no per-value limit. Beware that opening a CSV in Excel applies the same 32,767-character cell limit on display, so the file must be read by code, not double-clicked. Excel vs CSV vs Parquet for Python Data Pipelines compares the options.

Common pitfalls

  • Ignoring pandas warnings. One UserWarning per cell is easy to miss; make it an error.
  • Not checking write_string. A return value of -2 means the text was cut.
  • Splitting mid-word or mid-character. Split at whitespace; the round-trip check proves nothing was lost.
  • Reading chunks back with NaN. Use keep_default_na=False so empty parts are empty strings.
  • Formulas have their own limit. Formula text is limited to 8,192 characters; long CONCAT or IF chains hit that first.

Conclusion

Excel cells hold 32,767 characters and every Python writer truncates longer text: openpyxl silently, xlsxwriter with a -2 return, pandas with a warning. Measure string lengths before writing and turn truncation into an error, then keep the full text by splitting it into continuation cells that reassemble exactly, by writing previews that link to text files, or by using a format without the limit when the reader is a program.

Frequently asked questions

What is the maximum number of characters in an Excel cell? 32,767 characters. Excel shows only the first 1,024 or so in the cell itself, but stores and edits up to the limit.

Why is my text cut off when I write it with openpyxl? openpyxl truncates strings longer than 32,767 characters when they are assigned to a cell, without a warning. Check lengths before writing.

How do I make pandas fail instead of truncating long text? Wrap to_excel in warnings.catch_warnings() with warnings.filterwarnings("error", message="Cell contents too long"), so the truncation warning is raised as an exception.

How do I store text longer than 32,767 characters in Excel? Split it into chunks across several cells and join them when reading, or keep the full text in separate files and link to them from the workbook.

More on errors & fixes

More on errors & fixes →