DEV Community

abbazs
abbazs

Posted on • Edited on

Formatting Dates in Excel with openpyxl

Formatting Dates in Excel with openpyxl

When you write dates to Excel with Python, they show up as raw serial numbers by default. Excel stores dates as numbers internally, so the value is correct but the display format is missing.

The problem

import openpyxl

wb = openpyxl.Workbook()
ws = wb.active
ws.append(["2024-03-15"])
wb.save("output.xlsx")
Enter fullscreen mode Exit fullscreen mode

Open that file and you'll see 45380. The fix is applying a number_format to the cells.

Fix it with number_format

Set the format directly on each cell. The value must be a datetime or date object, not a string.

from datetime import date
import openpyxl

wb = openpyxl.Workbook()
ws = wb.active

ws.append(["Order Date", "Shipped Date"])
ws.append([date(2024, 1, 15), date(2024, 2, 28)])
ws.append([date(2024, 3, 1), date(2024, 3, 15)])

for row in ws.iter_rows(min_row=2, max_col=2):
    for cell in row:
        cell.number_format = "YYYY-MM-DD"

wb.save("orders.xlsx")
Enter fullscreen mode Exit fullscreen mode

number_format uses the same format codes as Excel's Format Cells dialog. The date is stored as a number, but Excel displays it in the format you specify.

Common format codes

Format code Displays as
YYYY-MM-DD 2024-03-15
MM/DD/YYYY 03/15/2024
DD-MMM-YYYY 15-Mar-2024
MMM YYYY Mar 2024
MMMM DD, YYYY March 15, 2024
YYYY 2024
MM/DD/YYYY HH:MM 03/15/2024 14:30

Reusable styles with NamedStyle

Define a style once and apply it to any cell in the workbook. Register it once with add_named_style (calling it again raises a ValueError).

from datetime import date
from openpyxl.styles import NamedStyle

wb = openpyxl.Workbook()
date_style = NamedStyle(name="iso_date", number_format="YYYY-MM-DD")
wb.add_named_style(date_style)

for sheet_name in ["Jan", "Feb", "Mar"]:
    ws = wb.create_sheet(sheet_name)
    ws.append(["Date", "Amount"])
    ws.append([date(2024, 1, 15), 1200])
    ws.append([date(2024, 1, 16), 3400])
    ws["A2"].style = date_style
    ws["A3"].style = date_style

del wb["Sheet"]
wb.save("quarterly.xlsx")
Enter fullscreen mode Exit fullscreen mode

With pandas

df.to_excel() accepts a date_format parameter in pandas 2.x but this was removed in pandas 3.0. To format dates in pandas 3.0+, write the DataFrame and then apply formatting with openpyxl.

import pandas as pd
from openpyxl import load_workbook
from datetime import datetime

df = pd.DataFrame({
    "date": pd.date_range("2024-01-01", periods=5),
    "value": [10, 20, 30, 40, 50],
})

df.to_excel("report.xlsx", index=False)

# Apply date formatting after writing
wb = load_workbook("report.xlsx")
ws = wb.active
for row in ws.iter_rows(min_row=2, max_col=1):
    row[0].number_format = "YYYY-MM-DD"
wb.save("report.xlsx")
Enter fullscreen mode Exit fullscreen mode

With polars

Polars uses xlsxwriter under the hood for Excel export. Install it with pip install xlsxwriter. After writing, open the file with openpyxl to apply formatting.

import polars as pl
from datetime import date
from openpyxl import load_workbook

dates = pl.date_range(date(2024, 1, 1), date(2024, 1, 5), eager=True)
df = pl.DataFrame({"date": dates, "value": [10, 20, 30, 40, 50]})

df.write_excel("report.xlsx")

wb = load_workbook("report.xlsx")
ws = wb.active
ws["A2"].number_format = "YYYY-MM-DD"
wb.save("report.xlsx")
Enter fullscreen mode Exit fullscreen mode

pl.date_range() returns a lazy expression by default. Pass eager=True to evaluate it immediately and get a Series.

Mixed formats in one sheet

Different columns can use different formats. A realistic example for a financial report:

from datetime import date
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side

wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Transactions"

header_font = Font(bold=True)
thin_border = Border(
    left=Side(style="thin"),
    right=Side(style="thin"),
    top=Side(style="thin"),
    bottom=Side(style="thin"),
)

headers = ["Date", "Description", "Amount"]
data = [
    [date(2024, 1, 15), "Office supplies", 245.00],
    [date(2024, 2, 28), "Consulting fee", 3500.00],
    [date(2024, 3, 1), "Software license", 1200.00],
    [date(2024, 6, 30), "Annual renewal", 960.00],
]

ws.append(headers)
for row in data:
    ws.append(row)

for cell in ws[1]:
    cell.font = header_font
    cell.border = thin_border
    cell.alignment = Alignment(horizontal="center")

for row in ws.iter_rows(min_row=2, max_row=len(data) + 1):
    row[0].number_format = "DD-MMM-YYYY"
    row[2].number_format = '$#,##0.00'
    for cell in row:
        cell.border = thin_border

ws.column_dimensions["A"].width = 16
ws.column_dimensions["B"].width = 20
ws.column_dimensions["C"].width = 14

wb.save("transactions.xlsx")
Enter fullscreen mode Exit fullscreen mode

Date column gets DD-MMM-YYYY, amount column gets currency format. The rest is standard spreadsheet styling.

Reading formatted dates back

openpyxl returns the raw datetime object regardless of display format. The format is for display only.

from openpyxl import load_workbook

wb = load_workbook("orders.xlsx")
ws = wb.active

for row in ws.iter_rows(min_row=2, values_only=True):
    print(f"{row[0]} | {row[1]}")
Enter fullscreen mode Exit fullscreen mode

Output:

2024-01-15 00:00:00 | 2024-02-28 00:00:00
2024-03-01 00:00:00 | 2024-03-15 00:00:00
Enter fullscreen mode Exit fullscreen mode

To get the formatted string instead:

cell = ws["A2"]
if cell.value:
    print(cell.value.strftime("%Y-%m-%d"))
Enter fullscreen mode Exit fullscreen mode

Top comments (0)