Wrap Text and Align Cells with openpyxl
Long text in a report — comments, descriptions, addresses, ticket summaries — either spills across neighbouring empty cells or, once the neighbour holds a value, is cut off at the column edge. Wrapping fixes that, but it brings its own questions: how tall should the row be, where should text sit in a tall row, and what about the short cells next to it? openpyxl controls all of it through the cell's Alignment. This guide, part of Styling Excel Cells with openpyxl, wraps and aligns a ticket list, sizes the rows, and covers indentation, rotation and centring without merging.
Prerequisites
pip install openpyxl
A ticket list with a mix of long, short and multi-line text:
import math
from copy import copy
from openpyxl import Workbook
from openpyxl.styles import Alignment, Font
tickets = [
("T-1", "High", "Customer reports that the monthly export stops at row 65,536 when opened in the old viewer."),
("T-2", "Low", "Short note"),
("T-3", "Medium", "Line one\nLine two\nLine three"),
]
wb = Workbook()
ws = wb.active
ws.title = "Tickets"
ws.append(["Ticket", "Priority", "Description"])
for ticket in tickets:
ws.append(ticket)
ws.column_dimensions["A"].width = 10
ws.column_dimensions["B"].width = 10
ws.column_dimensions["C"].width = 34
Step 1 — Wrap the text and align it to the top
Give every body cell an Alignment with wrap_text=True and vertical="top". Wrapping breaks the text at the column width and at newline characters; top alignment makes every cell in a tall row start on the same line:
for row in ws.iter_rows(min_row=2):
for cell in row:
cell.alignment = Alignment(wrap_text=True, vertical="top")
wb.save("tickets.xlsx")
print(ws["C4"].alignment.wrap_text, ws["C4"].alignment.vertical) # True top
Without vertical="top", Excel's default bottom alignment puts "T-1" and "High" level with the last line of a three-line description, which reads as though they belong to the row below. Top alignment is almost always right for lists with wrapped text. Newlines in the data — "Line one\nLine two" — only show as line breaks when wrapping is on; otherwise the text runs together.
Step 2 — Set row heights that fit
Whether rows grow to fit wrapped text depends on the application: rendering this file in LibreOffice adjusted the heights automatically, while Excel does not always recalculate heights for files it did not write itself. When the layout matters, set the heights. A good estimate counts the lines each cell needs at its column width:
def lines_needed(text, width_chars):
if text is None:
return 1
return sum(max(1, math.ceil(len(part) / max(width_chars - 1, 1))) for part in str(text).split("\n"))
def fit_row_heights(ws, line_height=15, min_row=2):
for row in ws.iter_rows(min_row=min_row):
lines = 1
for cell in row:
if cell.alignment.wrap_text:
width = ws.column_dimensions[cell.column_letter].width or 8.43
lines = max(lines, lines_needed(cell.value, width))
ws.row_dimensions[row[0].row].height = lines * line_height
fit_row_heights(ws)
wb.save("tickets.xlsx")
print([ws.row_dimensions[r].height for r in range(2, 5)]) # [45.0, 15.0, 45.0]
Column width in openpyxl is measured in characters of the default font, and a line of 11-point Calibri is about 15 points tall, so the estimate is close for ordinary text. Wide letters and bold text need a little more room; add a line of slack for descriptions where clipping would hide information. Column widths themselves are covered in Set Column Width and Row Height in openpyxl.
Step 3 — Horizontal alignment that matches the content
Text reads best left-aligned and numbers right-aligned — which is Excel's default for each type — so the main job is to leave them alone and fix the exceptions. Short codes such as priorities look tidier centred, and headers should align with the data beneath them:
ws["B1"].alignment = Alignment(horizontal="center", vertical="bottom")
for (cell,) in ws.iter_rows(min_row=2, min_col=2, max_col=2):
cell.alignment = Alignment(horizontal="center", vertical="top", wrap_text=True)
for cell in ws[1]:
font = copy(cell.font)
font.bold = True
cell.font = font
wb.save("tickets.xlsx")
Every assignment replaces the whole Alignment, so repeat vertical="top" and wrap_text when changing only the horizontal setting — or copy the existing alignment and change one attribute, the same pattern used for fonts. A header over a numeric column should be right-aligned so it sits above the digits it describes; a centred header over right-aligned numbers looks slightly off, especially when the values vary in length.
Step 4 — Indent hierarchies and rotate narrow headers
Two less common settings solve specific layout problems. indent shows a hierarchy in one column — categories and their items — without extra columns. text_rotation turns long headers above narrow columns sideways:
ws2 = wb.create_sheet("Budget")
for label, level in [("Operations", 0), ("Rent", 1), ("Utilities", 1), ("Staff", 0), ("Salaries", 1)]:
ws2.append([label])
ws2.cell(ws2.max_row, 1).alignment = Alignment(horizontal="left", indent=level * 2)
ws2["C1"], ws2["D1"] = "Actual this month", "Budget this month"
for cell in (ws2["C1"], ws2["D1"]):
cell.alignment = Alignment(text_rotation=90, horizontal="center", vertical="bottom")
ws2.row_dimensions[1].height = 110
ws2.column_dimensions["C"].width = ws2.column_dimensions["D"].width = 6
wb.save("tickets.xlsx")
print(ws2["A2"].alignment.indent, ws2["C1"].alignment.text_rotation) # 2.0 90
Indent levels are counted in steps of about three characters; two levels per hierarchy step reads clearly. Rotated headers need a taller header row — set it explicitly, because the rotated text will not grow the row in every application. text_rotation takes 0 to 90 for text rising to the right and 91 to 180 for text falling, where 180 means −90°; 255 stacks the letters vertically, one under another, which is rarely easier to read than simple rotation.
Step 5 — Centre a title without merging
A report title centred over the table is usually made with merged cells, which then get in the way of sorting, selecting and copying. centerContinuous centres text across the selected cells while keeping them separate — set it on the title cell and on the empty cells it should span:
ws.insert_rows(1)
ws["A1"] = "Open tickets"
for col in "ABC":
ws[f"{col}1"].alignment = Alignment(horizontal="centerContinuous")
for r in range(2, ws.max_row + 1): # heights did not move with the rows
ws.row_dimensions[r].height = None
fit_row_heights(ws, min_row=3)
wb.save("tickets.xlsx")
print(ws["A1"].value, ws["C1"].alignment.horizontal) # Open tickets centerContinuous
print([ws.row_dimensions[r].height for r in range(3, 6)]) # [45.0, 15.0, 45.0]
The title appears centred across A1:C1, yet clicking C1 selects an ordinary empty cell. Note the extra lines: insert_rows moves cell values and styles down but leaves row heights at their old row numbers, so the 45-point height meant for T-1 would now sit on the header row. Clearing the heights and fitting them again after any row insertion keeps them with their rows. The trade-offs against a real merge are covered in Merge Cells and Centre a Report Title with openpyxl. The last option, shrink_to_fit=True, reduces the font size until the text fits the cell; it suits labels that are only slightly too long, and becomes unreadable for anything longer — wrap instead.
Common pitfalls
- Vertical alignment on the header only. Aligning body cells to the top but leaving the header at the bottom is fine; the reverse — body at the bottom — is what makes tall rows hard to scan.
- Wrapping without widths. A wrapped cell in a default-width column breaks after eight characters. Set a sensible column width first.
- Replacing alignment piecemeal.
Alignment(horizontal="center")resetswrap_textandvertical. Pass all the settings a cell needs, or copy and modify. - Inserting rows after sizing them. Row heights stay at their row numbers when rows are inserted or deleted. Size rows last, after the sheet's structure is final.
- Wrapping numbers. Wrapping a numeric column never helps; a number too wide for its column shows as
####. Widen the column or change the number format.
Conclusion
Wrap long text with Alignment(wrap_text=True, vertical="top"), and set row heights from an estimate of the wrapped lines so the layout does not depend on whether the application autofits. Keep text left and numbers right, centre short codes, indent hierarchies, rotate headers over narrow columns and centre titles with centerContinuous instead of merging. Remember that each assignment replaces the whole alignment, so pass every setting a cell needs.
Frequently asked questions
How do I wrap text in a cell with openpyxl? Assign an Alignment with wrap_text=True, for example cell.alignment = Alignment(wrap_text=True, vertical="top"). The text then breaks at the column width and at any newline characters in the value.
Why do my wrapped rows not get taller? Whether rows grow automatically depends on the application. LibreOffice adjusted rows without a set height in our test; Excel does not always recalculate heights for files it did not write. Set row_dimensionsrow.height explicitly when the layout matters.
How do I centre a title across several columns without merging? Put the text in the first cell and set horizontal="centerContinuous" on that cell and the empty cells to its right. The title is centred across them, while the cells stay separate for sorting and selection.
How do I indent text in a cell? Use Alignment(indent=n) with a horizontal alignment of left or right. Each level indents by roughly the width of three characters, which suits category and sub-category labels.
Related
- Up one level: Styling Excel Cells with openpyxl — fonts, fills, borders and layout.
- Add Borders to a Range with openpyxl — structure for the same tables.
- Freeze the Header Row in Excel with openpyxl — keeping headers visible over long lists.
- Write Rich Text with Mixed Formats in One Cell with xlsxwriter — multi-line cells with mixed formatting.
More on cell styling & layout
- How-toWrite Rich Text with Mixed Formats in One Cell with xlsxwriter
- How-toCopy Cell Styles from One Cell to Another with openpyxl
- How-toAuto-Fit Column Widths When Writing with pandas
- How-toExport a Styled pandas DataFrame to Excel
- How-toGenerate a Staff Rota in Excel with Python
- How-toAdd a Table of Contents Sheet with Hyperlinks in Excel