How pandas Data Types Are Written to Excel Cells
DataFrame.to_excel looks like a straight copy from DataFrame to worksheet, but Excel only has four kinds of cell value — number, text, boolean and error — plus number formats that make some numbers look like dates. Every pandas type has to be squeezed into one of those, and some of the choices pandas makes are surprising: durations become day counts that display as whole numbers, infinity becomes the text inf, a 17-digit integer is silently rounded, and a timezone-aware column refuses to write at all. Knowing the mapping lets you convert columns before writing, so the workbook holds what you meant rather than what was convenient. Everything here was checked by writing each type with both engines and reading the cells back with openpyxl. It belongs to Writing DataFrames to Excel with pandas.
Prerequisites
pip install pandas openpyxl xlsxwriter
The results were the same for engine="xlsxwriter" (the default when it is installed) and engine="openpyxl" on pandas 3.0, openpyxl 3.1 and XlsxWriter 3.2.
Step 1 — Write one column of every type
import datetime as dt
import decimal
import numpy as np
import pandas as pd
samples = {
"int64": pd.Series([1, 2], dtype="int64"),
"Int64 with NA": pd.Series([1, None], dtype="Int64"),
"float with NaN": [1.5, np.nan],
"bool": [True, False],
"str": ["a", None],
"datetime64": pd.to_datetime(["2026-01-05 13:30", "2026-02-01 00:00"]),
"date objects": [dt.date(2026, 1, 5), dt.date(2026, 2, 1)],
"timedelta": pd.to_timedelta(["1 days 06:00:00", "0 days 00:30:00"]),
"category": pd.Categorical(["x", "y"]),
"Decimal": [decimal.Decimal("1.10"), decimal.Decimal("2.005")],
"inf": [np.inf, -np.inf],
"Period": pd.period_range("2026-01", periods=2, freq="M"),
"list": [[1, 2], [3]],
"17-digit int": pd.Series([12345678901234567, 2], dtype="int64"),
}
frame = pd.DataFrame(samples)
frame.to_excel("types.xlsx", index=False)
Step 2 — Read back what each cell holds
from openpyxl import load_workbook
ws = load_workbook("types.xlsx").active
print(f"{'column':15} {'stored value':28} {'type':4} number format")
for col in ws.iter_cols(min_row=1, max_row=3):
header, first, second = col
print(f"{header.value:15} {first.value!r:28} {first.data_type:4} {first.number_format} next: {second.value!r}")
openpyxl's data_type is n for numbers (dates included — a date is a number with a date format), s for text, b for booleans and d when it recognises a date format on a number. Here is what came back:
Integers, floats, booleans and plain text behave exactly as you would hope, and missing values (NaN, <NA>, None) become empty cells by default. The rest of this page goes through the types that need attention.
Step 3 — Dates and datetimes
A datetime64 column is written as an Excel date serial with the format YYYY-MM-DD HH:MM:SS, so even values at midnight display 00:00:00. A column of Python date objects (object dtype) gets YYYY-MM-DD. Choose the display yourself through the writer:
with pd.ExcelWriter("dates.xlsx", engine="xlsxwriter",
datetime_format="dd mmm yyyy hh:mm", date_format="dd mmm yyyy") as xw:
frame[["datetime64", "date objects"]].to_excel(xw, index=False)
ws = load_workbook("dates.xlsx").active
print(ws["A2"].number_format, "|", ws["B2"].number_format)
datetime_format applies to datetime columns and date_format only to date objects. Both are honoured by the xlsxwriter engine; the openpyxl engine ignores datetime_format, so either use xlsxwriter or set cell.number_format afterwards. To drop the time part entirely, convert with .dt.date before writing, as described in Write Dates Without Time from pandas to Excel. Timezone-aware columns are rejected with ValueError: Excel does not support datetimes with timezones; convert to the zone the reader expects and remove the zone with .dt.tz_convert("Europe/Berlin").dt.tz_localize(None).
Step 4 — Durations
A timedelta is stored as a fraction of days — 1 day 6 hours becomes 1.25, 30 minutes becomes 0.0208 — with the number format 0, which rounds the display to a whole number. Excel therefore shows 1 and 0. The value is right; the format hides it. Give the column a duration format:
with pd.ExcelWriter("durations.xlsx", engine="xlsxwriter") as xw:
frame[["timedelta"]].to_excel(xw, index=False)
hours = xw.book.add_format({"num_format": "[h]:mm"})
for row in range(1, 3):
xw.sheets["Sheet1"].write_number(row, 0, frame["timedelta"][row - 1] / pd.Timedelta(days=1), hours)
print(load_workbook("durations.xlsx").active["A2"].number_format)
[h]:mm shows 30:00 for 1.25 days; the square brackets stop hours wrapping at 24. Rewriting the cells is needed because pandas' per-cell format overrides a column format set with set_column. If the reader wants plain numbers instead, write hours as floats: frame["timedelta"].dt.total_seconds() / 3600. More formats are in Format Durations Over 24 Hours in Excel.
Step 5 — Numbers that do not survive
Excel stores every number as a 64-bit float, which holds about 15–16 significant digits. A 17-digit integer such as an order or card-like ID is written rounded — 12345678901234567 came back as 1.234567890123457e+16 — and nothing on the Excel side can restore it. Decimal values are converted to float on the way in, so 2.005 is stored as the nearest binary float, which is fine for display but not for exact accounting comparisons. Infinity is written as the text inf (controlled by inf_rep), so a numeric column suddenly contains strings that break SUM. Fix all three before writing:
fixed = frame.assign(**{
"17-digit int": frame["17-digit int"].astype(str),
"Decimal": frame["Decimal"].map(lambda d: float(round(d, 2))),
"inf": frame["inf"].replace([np.inf, -np.inf], np.nan),
})
fixed[["17-digit int", "Decimal", "inf"]].to_excel("numbers.xlsx", index=False)
for row in load_workbook("numbers.xlsx").active.iter_rows(min_row=2, values_only=True):
print(row)
Long identifiers are written as text so every digit survives; decimals are rounded to the precision the report needs; infinities become empty cells (or pass inf_rep="#N/A" if you prefer a visible marker). See Fix Long ID Numbers Losing Digits in Excel and Control How NaN and Infinity Appear in Excel Output.
Step 6 — Objects that turn into text
Anything pandas cannot map to a number, boolean or date is written with str(): a Period becomes "2026-01", a list becomes "[1, 2]", a dict its repr. Categoricals write their labels, which is usually what you want. Convert periods to real dates so Excel can sort and chart them, and join lists into readable text:
tidy = pd.DataFrame({
"Month": frame["Period"].dt.to_timestamp(),
"Items": frame["list"].map(lambda xs: ", ".join(map(str, xs))),
})
with pd.ExcelWriter("tidy.xlsx", engine="xlsxwriter", datetime_format="mmm yyyy") as xw:
tidy.to_excel(xw, index=False)
ws = load_workbook("tidy.xlsx").active
print(ws["A2"].value, ws["A2"].number_format, "|", ws["B2"].value)
Step 7 — Text that changes meaning on the way in
Plain strings are the safest type, with two exceptions worth testing for. A string that starts with = is written as a formula by both engines — "=1+1" arrives as a formula cell, not as text — which is how formula injection gets into exported files. And the xlsxwriter engine turns strings that look like URLs into clickable hyperlinks, while the openpyxl engine leaves them as text:
risky = pd.DataFrame({"s": ["=1+1", "00123", "http://example.org", "1e5"]})
for engine in ("xlsxwriter", "openpyxl"):
risky.to_excel("risky.xlsx", index=False, engine=engine)
cells = load_workbook("risky.xlsx").active["A"][1:]
print(engine, [(c.value, c.data_type) for c in cells], "link:", cells[2].hyperlink is not None)
Both engines store "=1+1" with data type f. Numeric-looking text such as "00123" and "1e5" stays text, so leading zeros survive. Neutralise user-supplied values that begin with =, +, - or @ before writing, as described in Prevent Formula Injection in Excel Exports.
Common pitfalls
- Trusting the display. A timedelta cell showing
1holds1.25; check the number format before concluding the data is wrong. - Setting a column format after
to_excel. pandas writes a format on each datetime and timedelta cell, which wins overset_column; pass formats toExcelWriteror rewrite the cells. - IDs as numbers. Anything longer than 15 digits, or with leading zeros, must be text before writing.
- Infinity in calculated columns. Ratios with a zero denominator produce
inf, which is written as text and breaks totals. - Assuming the engines differ. For values they behave the same; the difference is which writer options they honour (
datetime_formatonly works with xlsxwriter).
Performance notes
The conversions above are vectorised and cost far less than writing the file. Avoid per-row .map on large frames where a vectorised alternative exists — dt.total_seconds() instead of a lambda, astype(str) instead of formatting each value. Object columns of lists or dicts slow to_excel because each value is converted with str() individually; convert them once beforehand.
Conclusion
Excel cells hold numbers, text, booleans and errors; pandas maps integers, floats, booleans and strings cleanly, writes datetimes as date serials with a full date-time format, durations as day fractions formatted to hide the fraction, infinity as text, Decimal as float, and unknown objects with str(). Long integers lose digits and timezone-aware datetimes fail. A short pre-write pass — formats on the writer, hours or [h]:mm for durations, text for long IDs, NaN for infinity, real dates for periods — makes the workbook hold exactly what the DataFrame meant.
Frequently asked questions
Why does my timedelta column show whole numbers in Excel? pandas writes timedeltas as fractions of a day with the number format 0, which rounds the display. The cell holds 1.25 for 1 day 6 hours; apply a format such as h:mm, or write hours as floats instead.
Why is infinity written as "inf" text in Excel? Excel has no infinity value, so to_excel writes the string given by inf_rep, which defaults to "inf". Replace infinities with NaN before writing, or pass inf_rep to choose another marker.
Does to_excel keep Decimal precision? No. Decimal values are converted to Python floats, so they are stored as the nearest binary floating-point number. Round them to the precision you need, or write them as text if exact digits matter.
How do I write timezone-aware datetimes to Excel? Convert them to the time zone the reader expects with dt.tz_convert, then remove the zone with dt.tz_localize(None). Excel has no time zone support, so pandas raises ValueError otherwise.
Related
- Up one level: Writing DataFrames to Excel with pandas — to_excel and ExcelWriter in depth.
- How openpyxl Stores Python Values in Cells — the same question one level down.
- Fix "Excel does not support datetimes with timezones" in pandas — the full fix for aware timestamps.
- Use Nullable Data Types When Reading Excel with pandas — the reverse trip, keeping types intact on read.
More on writing excel files
- How-toWrite Multiple DataFrames to One Excel File
- How-toWrite a Formatted Excel Report with xlsxwriter
- How-toFind and Replace Text in Excel with openpyxl
- How-toInsert and Delete Rows and Columns with openpyxl
- How-toopenpyxl: Append Data to an Existing Excel Sheet
- How-toReorder Columns to Match a Template Layout with pandas