Guide
Styling Excel Cells with openpyxl
Style Excel cells from Python with openpyxl: Font, PatternFill, Border, Alignment, header rows in a loop, merged titles, and reusable NamedStyles — all runnable.
A report that no one styles looks like a database dump. With openpyxl you set fonts, fills, borders, and alignment directly on cells, and because you are editing the Office Open XML file itself, those styles persist exactly as written. This page is part of Formatting and Charting Excel Reports with Python, and it walks through every styling object you reach for in a real reporting script. Each block runs in order against a sample workbook built in the first step.
Install and create a sample workbook
pip install openpyxl
Build a tiny sales sheet with a header row and a few data rows so the styling steps below have real cells to target:
from openpyxl import Workbook
wb = Workbook()
ws = wb.active
ws.title = "Sales"
ws.append(["Region", "Rep", "Units", "Revenue"])
for row in [
["North", "Alvarez", 120, 24000],
["South", "Boateng", 95, 19000],
["East", "Chen", 140, 28000],
["West", "Dubois", 80, 16000],
]:
ws.append(row)
wb.save("styled_report.xlsx")
print("Sample workbook created")
Font: bold, color, and size
Font controls typeface, weight, size, and color. Color is an 8-digit ARGB hex string (or a 6-digit RGB string, which openpyxl pads to full opacity). Assign a fresh Font to a cell's .font attribute — fonts are immutable, so you replace them rather than mutate them:
from openpyxl import load_workbook
from openpyxl.styles import Font
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
ws["A1"].font = Font(name="Calibri", size=12, bold=True, color="FFFFFF")
ws["D2"].font = Font(bold=True, italic=True, color="2E7D32") # green revenue
wb.save("styled_report.xlsx")
print("Fonts applied")
PatternFill: cell backgrounds
PatternFill paints a cell's background. You must pass a fill_type (use "solid"); without it the fill renders invisibly. fgColor is the visible color for a solid fill:
from openpyxl import load_workbook
from openpyxl.styles import PatternFill
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
header_fill = PatternFill(fill_type="solid", fgColor="4472C4") # blue bar
for cell in ws[1]:
cell.fill = header_fill
wb.save("styled_report.xlsx")
print("Header fill applied")
Border and Side: ruled cells
A Border is built from four Side objects — left, right, top, bottom. Each Side takes a style (such as "thin", "medium", or "double") and a color. Define one Side and reuse it on all four edges for a clean box:
from openpyxl import load_workbook
from openpyxl.styles import Border, Side
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
thin = Side(style="thin", color="999999")
box = Border(left=thin, right=thin, top=thin, bottom=thin)
for row in ws["A1:D5"]:
for cell in row:
cell.border = box
wb.save("styled_report.xlsx")
print("Borders applied")
Alignment: wrapping and centering
Alignment controls horizontal and vertical placement plus text wrapping. Set wrap_text=True to let long values flow onto multiple lines instead of spilling over. Center the header row both ways:
from openpyxl import load_workbook
from openpyxl.styles import Alignment
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
center = Alignment(horizontal="center", vertical="center", wrap_text=True)
for cell in ws[1]:
cell.alignment = center
# Right-align the numeric columns for a tidy ledger look
right = Alignment(horizontal="right")
for row in ws["C2:D5"]:
for cell in row:
cell.alignment = right
wb.save("styled_report.xlsx")
print("Alignment applied")
Style a header row in one loop
In a real generator the header is styled in a single pass that combines font, fill, and alignment. Build the style objects once, outside the loop, then apply them to every cell in row 1:
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
head_font = Font(bold=True, color="FFFFFF", size=12)
head_fill = PatternFill("solid", fgColor="4472C4")
head_align = Alignment(horizontal="center", vertical="center")
for cell in ws[1]:
cell.font = head_font
cell.fill = head_fill
cell.alignment = head_align
wb.save("styled_report.xlsx")
print("Header row styled in a loop")
Merge cells for a title bar
ws.merge_cells() joins a rectangular range into one logical cell. Set the value and styling on the top-left cell of the range — the others are emptied. Insert a row first so the title sits above the table:
from openpyxl import load_workbook
from openpyxl.styles import Font, Alignment
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
ws.insert_rows(1) # push the table down one row
ws.merge_cells("A1:D1") # title spans all four columns
title = ws["A1"]
title.value = "Q3 Regional Sales"
title.font = Font(bold=True, size=14, color="1F2937")
title.alignment = Alignment(horizontal="center", vertical="center")
wb.save("styled_report.xlsx")
print("Title bar merged")
Reuse a NamedStyle
When the same combination of font, fill, border, and alignment recurs, define a NamedStyle once, register it on the workbook, then apply it by name. Excel also exposes named styles to the user, and reusing one keeps the file smaller than repeating inline styles:
from openpyxl import load_workbook
from openpyxl.styles import NamedStyle, Font, PatternFill, Alignment
wb = load_workbook("styled_report.xlsx")
ws = wb["Sales"]
header_style = NamedStyle(name="report_header")
header_style.font = Font(bold=True, color="FFFFFF")
header_style.fill = PatternFill("solid", fgColor="305496")
header_style.alignment = Alignment(horizontal="center")
if "report_header" not in wb.named_styles:
wb.add_named_style(header_style)
for cell in ws[2]: # the column-header row (row 1 is the title)
cell.style = "report_header"
wb.save("styled_report.xlsx")
print("NamedStyle applied")
Why these styles survive
openpyxl edits the workbook XML directly, so every style you set is written into the file and reopens exactly as saved. That is the key difference from pandas.to_excel(), which writes plain unstyled cells (beyond a couple of header options) and discards any formatting you might expect. If you generate the data with pandas and need styling, write the values first, then reopen the file with openpyxl and apply styles — or skip pandas and build the sheet with openpyxl from the start.
For sizing and navigation, two companion pages go deeper: Set Column Width and Row Height in openpyxl covers ws.column_dimensions[...].width and row heights, and Freeze the Header Row in Excel with openpyxl covers ws.freeze_panes so the header stays visible while scrolling.
Frequently asked questions
Why does my PatternFill show up as blank?
You almost certainly omitted fill_type. A PatternFill with no type renders nothing. Use PatternFill("solid", fgColor="4472C4") or PatternFill(fill_type="solid", fgColor="4472C4").
Can I modify just one attribute of an existing Font?
No — Font, Fill, Border, and Alignment are immutable. Build a new object with the attributes you want and assign it. To carry over existing values, read them off the old object first: Font(name=cell.font.name, bold=True).
Why did my value disappear after merging cells? Only the top-left cell of a merged range keeps its value; the rest are cleared. Set the value and styling on that top-left cell, and merge before writing if order matters.
Do styles applied with openpyxl survive a round-trip through pandas?
No. pandas.to_excel() writes plain cells and ignores prior formatting. Apply openpyxl styles as the last step, after any pandas write, so nothing overwrites them.
What color format does openpyxl expect?
An ARGB hex string. Pass 8 digits ("FF4472C4") for explicit opacity, or 6 digits ("4472C4") and openpyxl treats it as fully opaque.
Conclusion
Styling in openpyxl comes down to four immutable objects — Font, PatternFill, Border/Side, and Alignment — assigned to a cell's matching attribute. Build them once outside your loops, apply them across rows or ranges, and reach for NamedStyle when a combination repeats. Because openpyxl writes straight to the XML, the result is exactly what reopens in Excel, with none of the formatting loss you get from a plain pandas export.
Where to go next
- Formatting and Charting Excel Reports with Python — the parent pillar tying styling, charts, number formats, and images together.
- Set Column Width and Row Height in openpyxl — size columns and rows so styled content fits.
- Freeze the Header Row in Excel with openpyxl — keep your styled header visible while scrolling.
- Creating Charts in Excel with openpyxl — turn the styled table into a chart.
- Applying Number and Date Formats in Excel — format the values inside your styled cells.
- Applying Conditional Formatting with openpyxl — let the data drive cell colors automatically.