Guide
Advanced Data Transformation And CleaningDeep dive

Fix Excel Serial Numbers Showing Instead of Dates

Your Excel file shows 45292 where a date should be. Fix it in Python with openpyxl number formats, pandas ExcelWriter date formats, and a repair pass for existing workbooks.

You open the workbook your script produced and the date column reads 45292, 45293, 45294. Nothing is broken — those are the right values. Excel stores every date as a count of days and decides at display time whether to render that number as a date, and your cells are missing the instruction that makes it do so. This guide fixes it three ways: at write time with openpyxl, at write time through pandas, and as a repair pass over a workbook you already have. It builds on the storage model explained in Working with Dates and Times in Excel Data.

Same value, two number formats, two very different cells A single stored value of 46249 branches to two rendered cells. Under the General number format Excel displays the bare number 46249, which is what users report as a bug. Under the yyyy-mm-dd number format the identical value displays as 2026-08-15. The data is untouched in both cases; only the display instruction differs. stored in the cell 46249 number_format = "General" 46249 number_format = "yyyy-mm-dd" 2026-08-15

Prerequisites

Bash
pip install pandas openpyxl xlsxwriter

Before fixing anything, confirm which of two situations you are in, because they need different fixes:

Python
import pandas as pd

df = pd.read_excel("report.xlsx")
print(df["invoice_date"].dtype)
print(df["invoice_date"].head(3).tolist())

If the dtype is datetime64[ns], the data is fine and only the display is wrong — go to Step 1. If the dtype is int64 or float64 and the values look like five-digit numbers, the column is genuinely numeric and needs converting first — go to Step 4.

Step 1 — Set the number format with openpyxl

number_format is a per-cell attribute holding an Excel format string. Setting it is the entire fix:

Python
from datetime import date
from openpyxl import load_workbook

wb = load_workbook("report.xlsx")
ws = wb["Sheet1"]

# Column B holds dates; skip the header row.
for row in ws.iter_rows(min_row=2, min_col=2, max_col=2):
    for cell in row:
        cell.number_format = "yyyy-mm-dd"

wb.save("report_fixed.xlsx")

Two refinements make this robust in a real script. First, find the column by header name rather than hard-coding B, so an inserted column does not silently misformat a different field. Second, widen the column — a date in a narrow column displays as #####, which people report as a different bug:

Python
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter

DATE_FORMATS = {
    "invoice_date": "yyyy-mm-dd",
    "due_date": "yyyy-mm-dd",
    "processed_at": "yyyy-mm-dd hh:mm",
}

def format_date_columns(path, dest, sheet=None, widths=True):
    """Apply date number formats to columns identified by their header."""
    wb = load_workbook(path)
    ws = wb[sheet] if sheet else wb.active

    headers = {
        str(c.value).strip(): c.column
        for c in ws[1] if c.value is not None
    }

    applied = []
    for name, fmt in DATE_FORMATS.items():
        col = headers.get(name)
        if col is None:
            continue
        for (cell,) in ws.iter_rows(min_row=2, min_col=col, max_col=col):
            cell.number_format = fmt
        if widths:
            # Leave room, or Excel renders ##### instead of the date.
            ws.column_dimensions[get_column_letter(col)].width = len(fmt) + 4
        applied.append(name)

    wb.save(dest)
    return applied

print("formatted:", format_date_columns("report.xlsx", "report_fixed.xlsx"))

The format strings you will reach for most:

Format stringRenders asUse for
yyyy-mm-dd2026-08-15Dates in any report crossing regions
yyyy-mm-dd hh:mm2026-08-15 18:04Timestamps
dd mmm yyyy15 Aug 2026Human-facing summaries
mmm yyyyAug 2026Month labels on a period column
[h]:mm32:15Durations over 24 hours
hh:mm:ss18:04:32Time of day only

Note that mm means months after a date part and minutes after an hour part — hh:mm is minutes, yyyy-mm is months. Getting that wrong produces a cell showing the month number where you wanted minutes, which is a genuinely confusing bug to read.

Step 2 — Format at write time through pandas

If your script writes the workbook, fix it there and skip the repair pass entirely. ExcelWriter takes workbook-wide defaults:

Python
import pandas as pd

df = pd.DataFrame({
    "order": [1001, 1002],
    "invoice_date": pd.to_datetime(["2026-08-15", "2026-08-16"]),
    "processed_at": pd.to_datetime(["2026-08-15 18:04", "2026-08-16 09:12"]),
})

with pd.ExcelWriter(
    "orders.xlsx",
    engine="xlsxwriter",
    date_format="yyyy-mm-dd",
    datetime_format="yyyy-mm-dd hh:mm",
) as writer:
    df.to_excel(writer, sheet_name="Orders", index=False)

date_format applies to date-only values and datetime_format to values carrying a time. Both work with the openpyxl engine as well.

For per-column control — different formats on different columns, plus widths — drop to the xlsxwriter objects the writer exposes:

Python
import pandas as pd

with pd.ExcelWriter("orders.xlsx", engine="xlsxwriter") as writer:
    df.to_excel(writer, sheet_name="Orders", index=False)

    book = writer.book
    sheet = writer.sheets["Orders"]

    day = book.add_format({"num_format": "yyyy-mm-dd"})
    stamp = book.add_format({"num_format": "yyyy-mm-dd hh:mm"})

    # set_column formats a whole column in one call — no per-cell loop.
    sheet.set_column("B:B", 13, day)
    sheet.set_column("C:C", 19, stamp)

One trap with pandas: to_excel replaces the target sheet rather than merging into it. Formatting applied to a sheet before a later to_excel call is discarded. Format after writing, never before.

Step 3 — Repair a workbook you did not write

Sometimes the file arrives already broken and you do not control the producer. A repair pass can find the date columns itself, by testing whether the values sit in a plausible date range:

Three tests before reformatting a column as a date A column must pass three gates before the repair pass changes its format. First, the values must be numeric rather than text. Second, they must fall inside a plausible serial window of roughly 20000 to 60000, covering 1954 to 2064. Third, the header name should contain a date-like word such as date, day, at or on. Passing all three applies the format; failing any one leaves the column untouched. a column of unknown type 1 · numeric? not text, not blank int or float cells 2 · in range? 20000 to 60000 about 1954 to 2064 3 · named like a date? date, day, _at, _on stops false positives all three pass → apply the format · any one fails → leave it alone a quantity column of 45,000 units would pass tests 1 and 2 but not 3

The third gate matters. Without it, a column of order quantities that happens to sit between 20,000 and 60,000 gets reformatted as dates — a repair that makes the file worse:

Python
import re
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter

DATE_NAME = re.compile(r"(date|day|_at$|_on$|timestamp)", re.I)
PLAUSIBLE = range(20_000, 60_001)      # roughly 1954 to 2064

def repair_date_formats(path, dest, fmt="yyyy-mm-dd", sample=200):
    """Find numeric columns that are really dates and give them a date format."""
    wb = load_workbook(path)
    fixed = []

    for ws in wb.worksheets:
        headers = {c.column: str(c.value or "").strip() for c in ws[1]}

        for col, name in headers.items():
            if not DATE_NAME.search(name):
                continue                         # gate 3

            values, numeric = 0, 0
            for (cell,) in ws.iter_rows(min_row=2, max_row=1 + sample,
                                        min_col=col, max_col=col):
                if cell.value is None:
                    continue
                values += 1
                if isinstance(cell.value, (int, float)) \
                        and int(cell.value) in PLAUSIBLE:
                    numeric += 1                 # gates 1 and 2

            # Require a clear majority, so one stray number cannot trigger it.
            if values and numeric / values > 0.9:
                for (cell,) in ws.iter_rows(min_row=2, min_col=col, max_col=col):
                    cell.number_format = fmt
                ws.column_dimensions[get_column_letter(col)].width = len(fmt) + 4
                fixed.append(f"{ws.title}!{name}")

    wb.save(dest)
    return fixed

print("repaired:", repair_date_formats("broken.xlsx", "repaired.xlsx"))

Returning the list of what it changed — rather than silently rewriting the file — is what makes this safe to run unattended. Log it, and a surprising result is visible instead of buried.

Step 4 — When the values really are numbers

If pandas reports int64 or float64, a number format alone is only half a fix: Excel will display dates, but the column stays numeric to every Python reader. Convert the values, then write and format:

Python
import pandas as pd

df = pd.read_excel("report.xlsx")

# Guard against nonsense before converting.
plausible = df["invoice_date"].between(20_000, 60_000)
if not plausible.all():
    print(f"{(~plausible).sum()} values outside the plausible serial range")

df.loc[plausible, "invoice_date"] = pd.to_datetime(
    df.loc[plausible, "invoice_date"], unit="D", origin="1899-12-30"
)
df["invoice_date"] = pd.to_datetime(df["invoice_date"], errors="coerce")

with pd.ExcelWriter("report_fixed.xlsx", engine="xlsxwriter",
                    date_format="yyyy-mm-dd") as writer:
    df.to_excel(writer, sheet_name="Orders", index=False)

The 1899-12-30 origin is not arbitrary — it cancels Excel's 1900 leap-year bug, as explained in the parent topic. Anchoring at 1900-01-01 leaves every date one day late.

Common pitfalls and fixes

SymptomCauseFix
Cell shows 45292No date number formatSet cell.number_format = "yyyy-mm-dd".
Cell shows #####Column too narrow for the formatted dateWiden with column_dimensions[...].width.
Format vanished after a pandas writeto_excel replaced the sheetFormat after writing, or via ExcelWriter arguments.
Month shown where minutes expectedmm after a date part means monthsUse hh:mm for time, yyyy-mm for year-month.
Every date one day lateOrigin anchored at 1900-01-01Use origin="1899-12-30".
Duration of 32 hours shows as 8:00hh wraps at 24Use [h]:mm.
A quantity column became datesRepair heuristic too looseRequire a date-like header name as well.
Format applied but pandas still sees numbersValues never convertedConvert with to_datetime before writing.

Performance and scale notes

Three ways to apply a date format, and how each scales Three approaches side by side. Assigning number_format per cell creates or looks up a style entry for every cell, so cost grows with row count and can approach Excel's ceiling of roughly sixty-four thousand distinct formats. A shared NamedStyle assigned to each cell still loops but reuses one style entry. xlsxwriter's set_column applies one format to the whole column in a single call, so the cost is constant regardless of how many rows there are. per-cell assignment cell.number_format = "..." a style lookup per cell cost grows with rows can hit the 64k format ceiling one shared NamedStyle cell.style = "iso_date" still a loop, one style no format explosion loop cost remains set_column sheet.set_column("B:B", w, f) one call, whole column constant cost independent of row count

Per-cell formatting is the slow path. Each cell.number_format = ... assignment in openpyxl creates or looks up a style entry, and on a column of half a million rows that adds real time and memory.

Two faster routes. With xlsxwriter, set_column applies one format to an entire column in a single call, independent of row count:

Python
import pandas as pd

big = pd.DataFrame({
    "id": range(500_000),
    "invoice_date": pd.date_range("2024-01-01", periods=500_000, freq="min"),
})

with pd.ExcelWriter("big.xlsx", engine="xlsxwriter") as writer:
    big.to_excel(writer, sheet_name="Data", index=False)
    fmt = writer.book.add_format({"num_format": "yyyy-mm-dd hh:mm"})
    writer.sheets["Data"].set_column("B:B", 19, fmt)

With openpyxl, define the style once and reuse the object rather than assigning a fresh string per cell — Excel caps a workbook at roughly 64,000 distinct cell formats, and a loop that creates a new one each iteration will eventually hit it:

Python
from openpyxl.styles import NamedStyle
from openpyxl import load_workbook

date_style = NamedStyle(name="iso_date", number_format="yyyy-mm-dd")

wb = load_workbook("report.xlsx")
wb.add_named_style(date_style)
ws = wb.active
for (cell,) in ws.iter_rows(min_row=2, min_col=2, max_col=2):
    cell.style = "iso_date"      # one shared style, not one per cell
wb.save("report_fixed.xlsx")

For genuinely large workbooks, the cheapest fix of all is not to create the problem: write with the correct date_format in the first place, using the streaming approach in writing large DataFrames with write-only mode, and no repair pass is ever needed.

Conclusion

A cell showing 45292 is a display problem, not a data problem. Set number_format on the cell with openpyxl, or date_format and datetime_format on a pandas ExcelWriter, and the same value renders as a date. Widen the column so it does not turn into #####. If the column is genuinely numeric rather than a formatted date, convert it with the 1899-12-30 origin before writing. And for repair passes over files you did not create, require a date-like header name as well as a plausible value range, so a column of quantities never gets reformatted as dates.

Frequently asked questions

Is my data wrong when Excel shows 45292? No. The value is correct — 45292 is Excel's internal representation of 1 January 2024. Only the cell's display format is missing, so setting number_format on the cell fixes it without touching the data.

Which number format string should I use?"yyyy-mm-dd" for an unambiguous date, "yyyy-mm-dd hh:mm" when the time matters, and "[h]:mm" for durations. Avoid locale-dependent forms like "mm/dd/yyyy" in reports that cross borders.

Why did my format disappear after pandas wrote the file?to_excel replaces the sheet rather than merging into it, so formatting applied beforehand is lost. Set date_format and datetime_format on the ExcelWriter instead, or apply the formats after writing.

Do I have to format every cell individually? No. Set the format once per column by iterating that column's cells with openpyxl, or use xlsxwriter's set_column, which applies one format to a whole column in a single call.

What if the values really are numbers rather than dates? Convert them first with pd.to_datetime(col, unit="D", origin="1899-12-30"), then write and format. A number format alone will make Excel display a plausible date, but the column will still be numeric to pandas.