[{"data":1,"prerenderedAt":2156},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl":2123,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl":2132,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl":2137,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl":2138},{"id":4,"title":5,"body":6,"dateModified":2089,"datePublished":2089,"description":2090,"extension":2091,"faq":2092,"keyword":2101,"kind":2102,"meta":2103,"navigation":292,"path":2112,"seo":2113,"slug":2116,"stem":2117,"topics":2118,"type":2121,"__hash__":2122},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl\u002Findex.md","Write Rich Text in a Cell with openpyxl",{"type":7,"value":8,"toc":2076},"minimark",[9,24,37,183,188,215,221,225,238,562,592,596,603,1200,1210,1388,1403,1407,1413,1579,1582,1586,1597,1762,1794,1906,1910,1917,1921,1973,1977,1989,1993,2013,2017,2023,2029,2035,2041,2045,2072],[10,11,12,13,17,18,23],"p",{},"Normal cell formatting applies to the whole cell: the entire value is bold, or red, or italic. Sometimes one word needs to stand out from the rest — the \"OVERDUE\" in a status line, the search term inside a long note, the units in a heading — or a chemical formula needs a real subscript. Excel supports this as ",[14,15,16],"strong",{},"rich text",": a cell value made of runs, each with its own font. openpyxl has supported writing and reading rich text since version 3.1. This page, part of ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl",", builds rich text by hand and from data, and shows the one setting that stops it being lost.",[10,25,26,27,31,32,36],{},"If you are writing a new file with xlsxwriter, its ",[28,29,30],"code",{},"write_rich_string"," does the same job; see ",[19,33,35],{"href":34},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-rich-text-in-a-cell-with-xlsxwriter\u002F","Write Rich Text with Mixed Formats in One Cell with xlsxwriter",".",[38,39,47,48,47,52,47,56,47,64,47,74,47,85,47,92,47,97,47,102,47,107,47,112,47,119,47,124,47,128,47,132,47,135,47,140,47,146,47,151,47,155,47,159,47,162,47,167,47,174,47,179],"svg",{"viewBox":40,"role":41,"ariaLabelledBy":42,"xmlns":45,"style":46},"84 0 592 512","img",[43,44],"rt-parts-t","rt-parts-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[49,50,51],"title",{"id":43},"How a rich text value is built",[53,54,55],"desc",{"id":44},"A CellRichText value is a sequence of parts. Plain strings use the cell's own font. TextBlock parts pair an InlineFont with a piece of text, such as a bold red OVERDUE. Together they form one cell value, for example Status: OVERDUE since 12 Sep.",[57,58],"rect",{"x":59,"y":60,"width":61,"height":62,"fill":63},"84","0","592","512","#ffffff",[57,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"100.0","16.0","560.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[75,76,78],"g",{"transform":77},"translate(368.0,29.0) scale(1.000)",[79,80],"path",{"d":81,"fill":82,"stroke":83,"style":84},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[86,87,91],"text",{"x":88,"y":89,"style":90},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","CellRichText",[86,93,96],{"x":88,"y":94,"style":95},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","the cell value — one sequence",[98,99],"line",{"x1":88,"y1":100,"x2":88,"y2":101,"stroke":72,"style":73},"114.0","136.0",[103,104],"polygon",{"points":105,"fill":106},"380.0,140.0 375.0,131.0 385.0,131.0","#5b5cf0",[57,108],{"x":66,"y":109,"width":68,"height":69,"rx":70,"fill":110,"stroke":111,"style":73},"145.0","#fee8f2","var(--accent,#f43f8f)",[75,113,115],{"transform":114},"translate(368.0,158.0) scale(1.000)",[79,116],{"d":117,"fill":82,"stroke":118,"style":84},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--accent-ink,#be185d)",[86,120,123],{"x":88,"y":121,"style":122},"204.5","font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","TextBlock(font, text)",[86,125,127],{"x":88,"y":126,"style":95},"224.0","e.g. bold red \"OVERDUE\"",[98,129],{"x1":88,"y1":130,"x2":88,"y2":131,"stroke":72,"style":73},"243.0","265.0",[103,133],{"points":134,"fill":106},"380.0,269.0 375.0,260.0 385.0,260.0",[57,136],{"x":66,"y":137,"width":68,"height":69,"rx":70,"fill":138,"stroke":139,"style":73},"274.0","#e7ebef","var(--line,#cdd5e6)",[75,141,143],{"transform":142},"translate(368.0,287.0) scale(1.000)",[79,144],{"d":117,"fill":82,"stroke":145,"style":84},"var(--text,#172033)",[86,147,150],{"x":88,"y":148,"style":149},"333.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","plain string",[86,152,154],{"x":88,"y":153,"style":95},"353.0","e.g. \"Status: \" — cell font",[98,156],{"x1":88,"y1":157,"x2":88,"y2":158,"stroke":72,"style":73},"372.0","394.0",[103,160],{"points":161,"fill":106},"380.0,398.0 375.0,389.0 385.0,389.0",[57,163],{"x":66,"y":164,"width":68,"height":69,"rx":70,"fill":165,"stroke":166,"style":73},"403.0","#d9f4f1","var(--teal,#0f9488)",[75,168,170],{"transform":169},"translate(368.0,416.0) scale(1.000)",[79,171],{"d":172,"fill":82,"stroke":173,"style":84},"M12 9a3 3 0 1 0 .01 0z M12 2.5v3 M12 18.5v3 M2.5 12h3 M18.5 12h3 M5.3 5.3l2.1 2.1 M16.6 16.6l2.1 2.1 M5.3 18.7l2.1-2.1 M16.6 7.4l2.1-2.1","var(--teal-ink,#0b6157)",[86,175,178],{"x":88,"y":176,"style":177},"462.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","InlineFont",[86,180,182],{"x":88,"y":181,"style":95},"482.0","b, i, color, sz, rFont, vertAlign",[184,185,187],"h2",{"id":186},"prerequisites","Prerequisites",[189,190,195],"pre",{"className":191,"code":192,"language":193,"meta":194,"style":194},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install \"openpyxl>=3.1\" pandas\n","bash","",[28,196,197],{"__ignoreMap":194},[198,199,201,205,209,212],"span",{"class":98,"line":200},1,[198,202,204],{"class":203},"sMTad","pip",[198,206,208],{"class":207},"srMev"," install",[198,210,211],{"class":207}," \"openpyxl>=3.1\"",[198,213,214],{"class":207}," pandas\n",[10,216,217,220],{},[28,218,219],{},"invoices.xlsx"," has three invoices with a status, the days overdue and a free-text note.",[184,222,224],{"id":223},"step-1-write-mixed-formatting-in-one-cell","Step 1 — Write mixed formatting in one cell",[10,226,227,228,230,231,234,235,237],{},"A ",[28,229,91],{}," is a sequence of parts. Plain strings are shown in the cell's own font; each ",[28,232,233],{},"TextBlock"," pairs an ",[28,236,178],{}," with the text it formats. Give inline fonts an explicit font name and size — a run without them falls back to the application's default, which in some programs is a serif font that looks out of place:",[189,239,243],{"className":240,"code":241,"language":242,"meta":194,"style":194},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.cell.rich_text import CellRichText, TextBlock\nfrom openpyxl.cell.text import InlineFont\n\ndef font(sz=11, **kw):\n    return InlineFont(rFont=\"Calibri\", sz=sz, **kw)\n\nwb = Workbook(); ws = wb.active\nws[\"A1\"] = CellRichText(\"Status: \", TextBlock(font(b=True, color=\"FFC00000\"), \"OVERDUE\"), \" since 12 Sep\")\nws[\"A2\"] = CellRichText(TextBlock(font(b=True, sz=14), \"Q3 report \"), TextBlock(font(i=True, color=\"FF808080\"), \"(draft)\"))\nws.column_dimensions[\"A\"].width = 36\nwb.save(\"rich_basics.xlsx\")\nprint(type(ws[\"A1\"].value).__name__, \"->\", str(ws[\"A1\"].value))\n","python",[28,244,245,261,274,287,294,323,356,361,377,432,494,511,522],{"__ignoreMap":194},[198,246,247,251,255,258],{"class":98,"line":200},[198,248,250],{"class":249},"s-kum","from",[198,252,254],{"class":253},"skGVy"," openpyxl ",[198,256,257],{"class":249},"import",[198,259,260],{"class":253}," Workbook\n",[198,262,264,266,269,271],{"class":98,"line":263},2,[198,265,250],{"class":249},[198,267,268],{"class":253}," openpyxl.cell.rich_text ",[198,270,257],{"class":249},[198,272,273],{"class":253}," CellRichText, TextBlock\n",[198,275,277,279,282,284],{"class":98,"line":276},3,[198,278,250],{"class":249},[198,280,281],{"class":253}," openpyxl.cell.text ",[198,283,257],{"class":249},[198,285,286],{"class":253}," InlineFont\n",[198,288,290],{"class":98,"line":289},4,[198,291,293],{"emptyLinePlaceholder":292},true,"\n",[198,295,297,300,304,307,310,314,317,320],{"class":98,"line":296},5,[198,298,299],{"class":249},"def",[198,301,303],{"class":302},"s_Opv"," font",[198,305,306],{"class":253},"(sz",[198,308,309],{"class":249},"=",[198,311,313],{"class":312},"sP0c6","11",[198,315,316],{"class":253},", ",[198,318,319],{"class":249},"**",[198,321,322],{"class":253},"kw):\n",[198,324,326,329,332,336,338,341,343,346,348,351,353],{"class":98,"line":325},6,[198,327,328],{"class":249},"    return",[198,330,331],{"class":253}," InlineFont(",[198,333,335],{"class":334},"sa561","rFont",[198,337,309],{"class":249},[198,339,340],{"class":207},"\"Calibri\"",[198,342,316],{"class":253},[198,344,345],{"class":334},"sz",[198,347,309],{"class":249},[198,349,350],{"class":253},"sz, ",[198,352,319],{"class":249},[198,354,355],{"class":253},"kw)\n",[198,357,359],{"class":98,"line":358},7,[198,360,293],{"emptyLinePlaceholder":292},[198,362,364,367,369,372,374],{"class":98,"line":363},8,[198,365,366],{"class":253},"wb ",[198,368,309],{"class":249},[198,370,371],{"class":253}," Workbook(); ws ",[198,373,309],{"class":249},[198,375,376],{"class":253}," wb.active\n",[198,378,380,383,386,389,391,394,397,400,403,405,408,410,413,415,418,421,424,426,429],{"class":98,"line":379},9,[198,381,382],{"class":253},"ws[",[198,384,385],{"class":207},"\"A1\"",[198,387,388],{"class":253},"] ",[198,390,309],{"class":249},[198,392,393],{"class":253}," CellRichText(",[198,395,396],{"class":207},"\"Status: \"",[198,398,399],{"class":253},", TextBlock(font(",[198,401,402],{"class":334},"b",[198,404,309],{"class":249},[198,406,407],{"class":312},"True",[198,409,316],{"class":253},[198,411,412],{"class":334},"color",[198,414,309],{"class":249},[198,416,417],{"class":207},"\"FFC00000\"",[198,419,420],{"class":253},"), ",[198,422,423],{"class":207},"\"OVERDUE\"",[198,425,420],{"class":253},[198,427,428],{"class":207},"\" since 12 Sep\"",[198,430,431],{"class":253},")\n",[198,433,435,437,440,442,444,447,449,451,453,455,457,459,462,464,467,470,473,475,477,479,481,483,486,488,491],{"class":98,"line":434},10,[198,436,382],{"class":253},[198,438,439],{"class":207},"\"A2\"",[198,441,388],{"class":253},[198,443,309],{"class":249},[198,445,446],{"class":253}," CellRichText(TextBlock(font(",[198,448,402],{"class":334},[198,450,309],{"class":249},[198,452,407],{"class":312},[198,454,316],{"class":253},[198,456,345],{"class":334},[198,458,309],{"class":249},[198,460,461],{"class":312},"14",[198,463,420],{"class":253},[198,465,466],{"class":207},"\"Q3 report \"",[198,468,469],{"class":253},"), TextBlock(font(",[198,471,472],{"class":334},"i",[198,474,309],{"class":249},[198,476,407],{"class":312},[198,478,316],{"class":253},[198,480,412],{"class":334},[198,482,309],{"class":249},[198,484,485],{"class":207},"\"FF808080\"",[198,487,420],{"class":253},[198,489,490],{"class":207},"\"(draft)\"",[198,492,493],{"class":253},"))\n",[198,495,497,500,503,506,508],{"class":98,"line":496},11,[198,498,499],{"class":253},"ws.column_dimensions[",[198,501,502],{"class":207},"\"A\"",[198,504,505],{"class":253},"].width ",[198,507,309],{"class":249},[198,509,510],{"class":312}," 36\n",[198,512,514,517,520],{"class":98,"line":513},12,[198,515,516],{"class":253},"wb.save(",[198,518,519],{"class":207},"\"rich_basics.xlsx\"",[198,521,431],{"class":253},[198,523,525,528,531,534,537,539,542,545,547,550,552,555,557,559],{"class":98,"line":524},13,[198,526,527],{"class":312},"print",[198,529,530],{"class":253},"(",[198,532,533],{"class":312},"type",[198,535,536],{"class":253},"(ws[",[198,538,385],{"class":207},[198,540,541],{"class":253},"].value).",[198,543,544],{"class":312},"__name__",[198,546,316],{"class":253},[198,548,549],{"class":207},"\"->\"",[198,551,316],{"class":253},[198,553,554],{"class":312},"str",[198,556,536],{"class":253},[198,558,385],{"class":207},[198,560,561],{"class":253},"].value))\n",[10,563,564,567,568,570,571,316,573,316,575,578,579,316,582,316,584,316,586,588,589,36],{},[28,565,566],{},"str()"," of a rich text value is its plain text, \"Status: OVERDUE since 12 Sep\" — useful for widths, searching and logging. Colours are ARGB hex strings, like everywhere else in openpyxl. ",[28,569,178],{}," takes the run-level font options: ",[28,572,402],{},[28,574,472],{},[28,576,577],{},"u"," for underline, ",[28,580,581],{},"strike",[28,583,412],{},[28,585,345],{},[28,587,335],{}," for the font name and ",[28,590,591],{},"vertAlign",[184,593,595],{"id":594},"step-2-generate-rich-text-from-data","Step 2 — Generate rich text from data",[10,597,598,599,602],{},"The useful case is building these values from a table: colour the status word by its value, and highlight a keyword wherever it appears in a note. ",[28,600,601],{},"re.split"," with a capturing group keeps the matches, so they can be wrapped in a formatted block while the rest stays plain:",[189,604,606],{"className":240,"code":605,"language":242,"meta":194,"style":194},"import re\nimport pandas as pd\nfrom openpyxl import load_workbook\n\nSTATUS = {\"overdue\": \"FFC00000\", \"disputed\": \"FFB4740A\", \"paid\": \"FF0B6157\"}\n\ndef status_line(status, days):\n    word = TextBlock(font(b=True, color=STATUS[status]), status.upper())\n    tail = f\" — {days} days\" if days else \"\"\n    return CellRichText(word, tail)\n\ndef highlight(text, term):\n    parts = re.split(f\"({re.escape(term)})\", text, flags=re.IGNORECASE)\n    return CellRichText(*[TextBlock(font(b=True, u=\"single\"), p) if p.lower() == term.lower() else p for p in parts if p])\n\ndf = pd.read_excel(\"invoices.xlsx\")\ndf.to_excel(\"invoices_rich.xlsx\", index=False)\nwb = load_workbook(\"invoices_rich.xlsx\", rich_text=True)\nws = wb.active\nfor row, r in enumerate(df.itertuples(index=False), start=2):\n    ws.cell(row=row, column=2).value = status_line(r.Status, r.Days)\n    ws.cell(row=row, column=4).value = highlight(r.Note, \"audit\")\nws.column_dimensions[\"B\"].width = 20\nws.column_dimensions[\"D\"].width = 44\nwb.save(\"invoices_rich.xlsx\")\nprint([str(ws.cell(row=i, column=2).value) for i in range(2, 5)])\nprint([len(ws.cell(row=i, column=4).value) for i in range(2, 5)], \"parts per note\")\n",[28,607,608,615,628,639,643,685,689,699,726,764,771,775,785,827,891,896,912,933,956,966,1001,1030,1060,1075,1090,1099,1150],{"__ignoreMap":194},[198,609,610,612],{"class":98,"line":200},[198,611,257],{"class":249},[198,613,614],{"class":253}," re\n",[198,616,617,619,622,625],{"class":98,"line":263},[198,618,257],{"class":249},[198,620,621],{"class":253}," pandas ",[198,623,624],{"class":249},"as",[198,626,627],{"class":253}," pd\n",[198,629,630,632,634,636],{"class":98,"line":276},[198,631,250],{"class":249},[198,633,254],{"class":253},[198,635,257],{"class":249},[198,637,638],{"class":253}," load_workbook\n",[198,640,641],{"class":98,"line":289},[198,642,293],{"emptyLinePlaceholder":292},[198,644,645,648,651,654,657,660,662,664,667,669,672,674,677,679,682],{"class":98,"line":296},[198,646,647],{"class":312},"STATUS",[198,649,650],{"class":249}," =",[198,652,653],{"class":253}," {",[198,655,656],{"class":207},"\"overdue\"",[198,658,659],{"class":253},": ",[198,661,417],{"class":207},[198,663,316],{"class":253},[198,665,666],{"class":207},"\"disputed\"",[198,668,659],{"class":253},[198,670,671],{"class":207},"\"FFB4740A\"",[198,673,316],{"class":253},[198,675,676],{"class":207},"\"paid\"",[198,678,659],{"class":253},[198,680,681],{"class":207},"\"FF0B6157\"",[198,683,684],{"class":253},"}\n",[198,686,687],{"class":98,"line":325},[198,688,293],{"emptyLinePlaceholder":292},[198,690,691,693,696],{"class":98,"line":358},[198,692,299],{"class":249},[198,694,695],{"class":302}," status_line",[198,697,698],{"class":253},"(status, days):\n",[198,700,701,704,706,709,711,713,715,717,719,721,723],{"class":98,"line":363},[198,702,703],{"class":253},"    word ",[198,705,309],{"class":249},[198,707,708],{"class":253}," TextBlock(font(",[198,710,402],{"class":334},[198,712,309],{"class":249},[198,714,407],{"class":312},[198,716,316],{"class":253},[198,718,412],{"class":334},[198,720,309],{"class":249},[198,722,647],{"class":312},[198,724,725],{"class":253},"[status]), status.upper())\n",[198,727,728,731,733,736,739,743,746,749,752,755,758,761],{"class":98,"line":379},[198,729,730],{"class":253},"    tail ",[198,732,309],{"class":249},[198,734,735],{"class":249}," f",[198,737,738],{"class":207},"\" — ",[198,740,742],{"class":741},"sSjpA","{",[198,744,745],{"class":253},"days",[198,747,748],{"class":741},"}",[198,750,751],{"class":207}," days\"",[198,753,754],{"class":249}," if",[198,756,757],{"class":253}," days ",[198,759,760],{"class":249},"else",[198,762,763],{"class":207}," \"\"\n",[198,765,766,768],{"class":98,"line":434},[198,767,328],{"class":249},[198,769,770],{"class":253}," CellRichText(word, tail)\n",[198,772,773],{"class":98,"line":496},[198,774,293],{"emptyLinePlaceholder":292},[198,776,777,779,782],{"class":98,"line":513},[198,778,299],{"class":249},[198,780,781],{"class":302}," highlight",[198,783,784],{"class":253},"(text, term):\n",[198,786,787,790,792,795,798,801,803,806,808,811,814,817,819,822,825],{"class":98,"line":524},[198,788,789],{"class":253},"    parts ",[198,791,309],{"class":249},[198,793,794],{"class":253}," re.split(",[198,796,797],{"class":249},"f",[198,799,800],{"class":207},"\"(",[198,802,742],{"class":741},[198,804,805],{"class":253},"re.escape(term)",[198,807,748],{"class":741},[198,809,810],{"class":207},")\"",[198,812,813],{"class":253},", text, ",[198,815,816],{"class":334},"flags",[198,818,309],{"class":249},[198,820,821],{"class":253},"re.",[198,823,824],{"class":312},"IGNORECASE",[198,826,431],{"class":253},[198,828,830,832,834,837,840,842,844,846,848,850,852,855,858,861,864,867,870,872,875,878,880,883,886,888],{"class":98,"line":829},14,[198,831,328],{"class":249},[198,833,393],{"class":253},[198,835,836],{"class":249},"*",[198,838,839],{"class":253},"[TextBlock(font(",[198,841,402],{"class":334},[198,843,309],{"class":249},[198,845,407],{"class":312},[198,847,316],{"class":253},[198,849,577],{"class":334},[198,851,309],{"class":249},[198,853,854],{"class":207},"\"single\"",[198,856,857],{"class":253},"), p) ",[198,859,860],{"class":249},"if",[198,862,863],{"class":253}," p.lower() ",[198,865,866],{"class":249},"==",[198,868,869],{"class":253}," term.lower() ",[198,871,760],{"class":249},[198,873,874],{"class":253}," p ",[198,876,877],{"class":249},"for",[198,879,874],{"class":253},[198,881,882],{"class":249},"in",[198,884,885],{"class":253}," parts ",[198,887,860],{"class":249},[198,889,890],{"class":253}," p])\n",[198,892,894],{"class":98,"line":893},15,[198,895,293],{"emptyLinePlaceholder":292},[198,897,899,902,904,907,910],{"class":98,"line":898},16,[198,900,901],{"class":253},"df ",[198,903,309],{"class":249},[198,905,906],{"class":253}," pd.read_excel(",[198,908,909],{"class":207},"\"invoices.xlsx\"",[198,911,431],{"class":253},[198,913,915,918,921,923,926,928,931],{"class":98,"line":914},17,[198,916,917],{"class":253},"df.to_excel(",[198,919,920],{"class":207},"\"invoices_rich.xlsx\"",[198,922,316],{"class":253},[198,924,925],{"class":334},"index",[198,927,309],{"class":249},[198,929,930],{"class":312},"False",[198,932,431],{"class":253},[198,934,936,938,940,943,945,947,950,952,954],{"class":98,"line":935},18,[198,937,366],{"class":253},[198,939,309],{"class":249},[198,941,942],{"class":253}," load_workbook(",[198,944,920],{"class":207},[198,946,316],{"class":253},[198,948,949],{"class":334},"rich_text",[198,951,309],{"class":249},[198,953,407],{"class":312},[198,955,431],{"class":253},[198,957,959,962,964],{"class":98,"line":958},19,[198,960,961],{"class":253},"ws ",[198,963,309],{"class":249},[198,965,376],{"class":253},[198,967,969,971,974,976,979,982,984,986,988,990,993,995,998],{"class":98,"line":968},20,[198,970,877],{"class":249},[198,972,973],{"class":253}," row, r ",[198,975,882],{"class":249},[198,977,978],{"class":312}," enumerate",[198,980,981],{"class":253},"(df.itertuples(",[198,983,925],{"class":334},[198,985,309],{"class":249},[198,987,930],{"class":312},[198,989,420],{"class":253},[198,991,992],{"class":334},"start",[198,994,309],{"class":249},[198,996,997],{"class":312},"2",[198,999,1000],{"class":253},"):\n",[198,1002,1004,1007,1010,1012,1015,1018,1020,1022,1025,1027],{"class":98,"line":1003},21,[198,1005,1006],{"class":253},"    ws.cell(",[198,1008,1009],{"class":334},"row",[198,1011,309],{"class":249},[198,1013,1014],{"class":253},"row, ",[198,1016,1017],{"class":334},"column",[198,1019,309],{"class":249},[198,1021,997],{"class":312},[198,1023,1024],{"class":253},").value ",[198,1026,309],{"class":249},[198,1028,1029],{"class":253}," status_line(r.Status, r.Days)\n",[198,1031,1033,1035,1037,1039,1041,1043,1045,1048,1050,1052,1055,1058],{"class":98,"line":1032},22,[198,1034,1006],{"class":253},[198,1036,1009],{"class":334},[198,1038,309],{"class":249},[198,1040,1014],{"class":253},[198,1042,1017],{"class":334},[198,1044,309],{"class":249},[198,1046,1047],{"class":312},"4",[198,1049,1024],{"class":253},[198,1051,309],{"class":249},[198,1053,1054],{"class":253}," highlight(r.Note, ",[198,1056,1057],{"class":207},"\"audit\"",[198,1059,431],{"class":253},[198,1061,1063,1065,1068,1070,1072],{"class":98,"line":1062},23,[198,1064,499],{"class":253},[198,1066,1067],{"class":207},"\"B\"",[198,1069,505],{"class":253},[198,1071,309],{"class":249},[198,1073,1074],{"class":312}," 20\n",[198,1076,1078,1080,1083,1085,1087],{"class":98,"line":1077},24,[198,1079,499],{"class":253},[198,1081,1082],{"class":207},"\"D\"",[198,1084,505],{"class":253},[198,1086,309],{"class":249},[198,1088,1089],{"class":312}," 44\n",[198,1091,1093,1095,1097],{"class":98,"line":1092},25,[198,1094,516],{"class":253},[198,1096,920],{"class":207},[198,1098,431],{"class":253},[198,1100,1102,1104,1107,1109,1112,1114,1116,1119,1121,1123,1125,1128,1130,1133,1135,1138,1140,1142,1144,1147],{"class":98,"line":1101},26,[198,1103,527],{"class":312},[198,1105,1106],{"class":253},"([",[198,1108,554],{"class":312},[198,1110,1111],{"class":253},"(ws.cell(",[198,1113,1009],{"class":334},[198,1115,309],{"class":249},[198,1117,1118],{"class":253},"i, ",[198,1120,1017],{"class":334},[198,1122,309],{"class":249},[198,1124,997],{"class":312},[198,1126,1127],{"class":253},").value) ",[198,1129,877],{"class":249},[198,1131,1132],{"class":253}," i ",[198,1134,882],{"class":249},[198,1136,1137],{"class":312}," range",[198,1139,530],{"class":253},[198,1141,997],{"class":312},[198,1143,316],{"class":253},[198,1145,1146],{"class":312},"5",[198,1148,1149],{"class":253},")])\n",[198,1151,1153,1155,1157,1160,1162,1164,1166,1168,1170,1172,1174,1176,1178,1180,1182,1184,1186,1188,1190,1192,1195,1198],{"class":98,"line":1152},27,[198,1154,527],{"class":312},[198,1156,1106],{"class":253},[198,1158,1159],{"class":312},"len",[198,1161,1111],{"class":253},[198,1163,1009],{"class":334},[198,1165,309],{"class":249},[198,1167,1118],{"class":253},[198,1169,1017],{"class":334},[198,1171,309],{"class":249},[198,1173,1047],{"class":312},[198,1175,1127],{"class":253},[198,1177,877],{"class":249},[198,1179,1132],{"class":253},[198,1181,882],{"class":249},[198,1183,1137],{"class":312},[198,1185,530],{"class":253},[198,1187,997],{"class":312},[198,1189,316],{"class":253},[198,1191,1146],{"class":312},[198,1193,1194],{"class":253},")], ",[198,1196,1197],{"class":207},"\"parts per note\"",[198,1199,431],{"class":253},[10,1201,1202,1203,1205,1206,1209],{},"pandas writes the table; openpyxl then replaces two columns with rich text. The notes that mention \"audit\" become two parts each — the bold underlined word and the text beside it, with the empty strings ",[28,1204,601],{}," leaves at either end filtered out — and the one without it stays a single plain part. Note the ",[28,1207,1208],{},"rich_text=True"," when loading: it is what keeps these values intact on the next save.",[38,1211,47,1217,47,1220,47,1223,47,1228,47,1233,47,1240,47,1244,47,1250,47,1254,47,1258,47,1262,47,1266,47,1270,47,1274,47,1278,47,1283,47,1285,47,1289,47,1291,47,1294,47,1296,47,1299,47,1301,47,1304,47,1306,47,1309,47,1311,47,1315,47,1318,47,1322,47,1324,47,1327,47,1329,47,1332,47,1335,47,1339,47,1341,47,1344,47,1346,47,1350,47,1352,47,1354,47,1356,47,1359,47,1362,47,1365,47,1367,47,1370,47,1374,47,1378,47,1380,47,1383,47,1385],{"viewBox":1212,"role":41,"ariaLabelledBy":1213,"xmlns":45,"style":1216},"40 0 680 200",[1214,1215],"rt-sheet-t","rt-sheet-d","width:100%;max-width:680px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[49,1218,1219],{"id":1214},"Status words and keywords formatted inside their cells",[53,1221,1222],{"id":1215},"The invoices sheet after adding rich text. The Status column shows OVERDUE in bold red followed by 26 days, PAID in bold green, and DISPUTED in bold amber followed by 72 days. In the Note column, the word audit is bold and underlined inside the first and third notes.",[57,1224],{"x":1225,"y":60,"width":1226,"height":1227,"fill":63},"40","680","200",[86,1229,1232],{"x":88,"y":1230,"style":1231},"31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","invoices_rich.xlsx",[57,1234],{"x":1235,"y":1236,"width":1237,"height":1238,"rx":60,"fill":138,"stroke":139,"style":1239},"56.2","48.0","32.0","24.0","stroke-width:1px",[57,1241],{"x":1242,"y":1236,"width":1243,"height":1238,"rx":60,"fill":138,"stroke":139,"style":1239},"88.2","85.3",[86,1245,1249],{"x":1246,"y":1247,"style":1248},"130.9","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[57,1251],{"x":1252,"y":1236,"width":1253,"height":1238,"rx":60,"fill":138,"stroke":139,"style":1239},"173.5","154.2",[86,1255,1257],{"x":1256,"y":1247,"style":1248},"250.6","B",[57,1259],{"x":1260,"y":1236,"width":1261,"height":1238,"rx":60,"fill":138,"stroke":139,"style":1239},"327.7","55.1",[86,1263,1265],{"x":1264,"y":1247,"style":1248},"355.2","C",[57,1267],{"x":1268,"y":1236,"width":1269,"height":1238,"rx":60,"fill":138,"stroke":139,"style":1239},"382.7","321.0",[86,1271,1273],{"x":1272,"y":1247,"style":1248},"543.2","D",[57,1275],{"x":1235,"y":1276,"width":1237,"height":1277,"rx":60,"fill":138,"stroke":139,"style":1239},"72.0","28.0",[86,1279,1282],{"x":1280,"y":1281,"style":1248},"72.2","91.0","1",[57,1284],{"x":1242,"y":1276,"width":1243,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1286,1288],{"x":1246,"y":1281,"style":1287},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Invoice",[57,1290],{"x":1252,"y":1276,"width":1253,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1292,1293],{"x":1256,"y":1281,"style":1287},"Status",[57,1295],{"x":1260,"y":1276,"width":1261,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1297,1298],{"x":1264,"y":1281,"style":1287},"Days",[57,1300],{"x":1268,"y":1276,"width":1269,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1302,1303],{"x":1272,"y":1281,"style":1287},"Note",[57,1305],{"x":1235,"y":66,"width":1237,"height":1277,"rx":60,"fill":138,"stroke":139,"style":1239},[86,1307,997],{"x":1280,"y":1308,"style":1248},"119.0",[57,1310],{"x":1242,"y":66,"width":1243,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1312,1314],{"x":1246,"y":1308,"style":1313},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","INV-2150",[57,1316],{"x":1252,"y":66,"width":1253,"height":1277,"rx":60,"fill":110,"stroke":111,"style":1317},"stroke-width:1.5px",[86,1319,1321],{"x":1256,"y":1308,"style":1320},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","OVERDUE — 26 days",[57,1323],{"x":1260,"y":66,"width":1261,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1325,1326],{"x":1264,"y":1308,"style":1313},"26",[57,1328],{"x":1268,"y":66,"width":1269,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1330,1331],{"x":1272,"y":1308,"style":1313},"Customer promised payment after the **audit**",[57,1333],{"x":1235,"y":1334,"width":1237,"height":1277,"rx":60,"fill":138,"stroke":139,"style":1239},"128.0",[86,1336,1338],{"x":1280,"y":1337,"style":1248},"147.0","3",[57,1340],{"x":1242,"y":1334,"width":1243,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1342,1343],{"x":1246,"y":1337,"style":1313},"INV-2201",[57,1345],{"x":1252,"y":1334,"width":1253,"height":1277,"rx":60,"fill":165,"stroke":166,"style":1317},[86,1347,1349],{"x":1256,"y":1337,"style":1348},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","PAID",[57,1351],{"x":1260,"y":1334,"width":1261,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1353,60],{"x":1264,"y":1337,"style":1313},[57,1355],{"x":1268,"y":1334,"width":1269,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1357,1358],{"x":1272,"y":1337,"style":1313},"Paid by bank transfer",[57,1360],{"x":1235,"y":1361,"width":1237,"height":1277,"rx":60,"fill":138,"stroke":139,"style":1239},"156.0",[86,1363,1047],{"x":1280,"y":1364,"style":1248},"175.0",[57,1366],{"x":1242,"y":1361,"width":1243,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1368,1369],{"x":1246,"y":1364,"style":1313},"INV-2098",[57,1371],{"x":1252,"y":1361,"width":1253,"height":1277,"rx":60,"fill":1372,"stroke":1373,"style":1317},"#fdefd8","var(--gold,#b4740a)",[86,1375,1377],{"x":1256,"y":1364,"style":1376},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","DISPUTED — 72 days",[57,1379],{"x":1260,"y":1361,"width":1261,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1381,1382],{"x":1264,"y":1364,"style":1313},"72",[57,1384],{"x":1268,"y":1361,"width":1269,"height":1277,"rx":60,"fill":63,"stroke":139,"style":1239},[86,1386,1387],{"x":1272,"y":1364,"style":1313},"**Audit** found a pricing error on line 3",[10,1389,1390,1391,1394,1395,1398,1399,1402],{},"Two other places rich text earns its keep in reports. A ",[14,1392,1393],{},"title cell"," can carry the report name in large bold type and the period or \"draft\" marker in smaller grey text, in one cell that stays aligned with the table below — no second row, no merged cells. And a ",[14,1396,1397],{},"legend or key"," above a table can show each status word in the colour the table uses for it, so readers learn the colour code without a separate chart. In both cases size the column from the plain text, ",[28,1400,1401],{},"len(str(cell.value))",", since the rich value's length is its number of parts, not characters.",[184,1404,1406],{"id":1405},"step-3-subscripts-and-superscripts","Step 3 — Subscripts and superscripts",[10,1408,1409,1410,1412],{},"Units and formulas need characters raised or lowered within the line — m², CO₂, footnote markers. ",[28,1411,591],{}," on an inline font does it, and unlike Unicode superscript characters it works for any text:",[189,1414,1416],{"className":240,"code":1415,"language":242,"meta":194,"style":194},"wb = Workbook(); ws = wb.active\nsub, sup = font(vertAlign=\"subscript\"), font(vertAlign=\"superscript\")\nws[\"A1\"] = CellRichText(\"Floor area (m\", TextBlock(sup, \"2\"), \")\")\nws[\"A2\"] = CellRichText(\"CO\", TextBlock(sub, \"2\"), \" emissions\")\nws[\"A3\"] = CellRichText(\"Revenue\", TextBlock(sup, \"1\"))\nwb.save(\"scripts.xlsx\")\nprint([str(ws[c].value) for c in (\"A1\", \"A2\", \"A3\")])\n",[28,1417,1418,1430,1459,1487,1514,1537,1546],{"__ignoreMap":194},[198,1419,1420,1422,1424,1426,1428],{"class":98,"line":200},[198,1421,366],{"class":253},[198,1423,309],{"class":249},[198,1425,371],{"class":253},[198,1427,309],{"class":249},[198,1429,376],{"class":253},[198,1431,1432,1435,1437,1440,1442,1444,1447,1450,1452,1454,1457],{"class":98,"line":263},[198,1433,1434],{"class":253},"sub, sup ",[198,1436,309],{"class":249},[198,1438,1439],{"class":253}," font(",[198,1441,591],{"class":334},[198,1443,309],{"class":249},[198,1445,1446],{"class":207},"\"subscript\"",[198,1448,1449],{"class":253},"), font(",[198,1451,591],{"class":334},[198,1453,309],{"class":249},[198,1455,1456],{"class":207},"\"superscript\"",[198,1458,431],{"class":253},[198,1460,1461,1463,1465,1467,1469,1471,1474,1477,1480,1482,1485],{"class":98,"line":276},[198,1462,382],{"class":253},[198,1464,385],{"class":207},[198,1466,388],{"class":253},[198,1468,309],{"class":249},[198,1470,393],{"class":253},[198,1472,1473],{"class":207},"\"Floor area (m\"",[198,1475,1476],{"class":253},", TextBlock(sup, ",[198,1478,1479],{"class":207},"\"2\"",[198,1481,420],{"class":253},[198,1483,1484],{"class":207},"\")\"",[198,1486,431],{"class":253},[198,1488,1489,1491,1493,1495,1497,1499,1502,1505,1507,1509,1512],{"class":98,"line":289},[198,1490,382],{"class":253},[198,1492,439],{"class":207},[198,1494,388],{"class":253},[198,1496,309],{"class":249},[198,1498,393],{"class":253},[198,1500,1501],{"class":207},"\"CO\"",[198,1503,1504],{"class":253},", TextBlock(sub, ",[198,1506,1479],{"class":207},[198,1508,420],{"class":253},[198,1510,1511],{"class":207},"\" emissions\"",[198,1513,431],{"class":253},[198,1515,1516,1518,1521,1523,1525,1527,1530,1532,1535],{"class":98,"line":296},[198,1517,382],{"class":253},[198,1519,1520],{"class":207},"\"A3\"",[198,1522,388],{"class":253},[198,1524,309],{"class":249},[198,1526,393],{"class":253},[198,1528,1529],{"class":207},"\"Revenue\"",[198,1531,1476],{"class":253},[198,1533,1534],{"class":207},"\"1\"",[198,1536,493],{"class":253},[198,1538,1539,1541,1544],{"class":98,"line":325},[198,1540,516],{"class":253},[198,1542,1543],{"class":207},"\"scripts.xlsx\"",[198,1545,431],{"class":253},[198,1547,1548,1550,1552,1554,1557,1559,1562,1564,1567,1569,1571,1573,1575,1577],{"class":98,"line":358},[198,1549,527],{"class":312},[198,1551,1106],{"class":253},[198,1553,554],{"class":312},[198,1555,1556],{"class":253},"(ws[c].value) ",[198,1558,877],{"class":249},[198,1560,1561],{"class":253}," c ",[198,1563,882],{"class":249},[198,1565,1566],{"class":253}," (",[198,1568,385],{"class":207},[198,1570,316],{"class":253},[198,1572,439],{"class":207},[198,1574,316],{"class":253},[198,1576,1520],{"class":207},[198,1578,1149],{"class":253},[10,1580,1581],{},"The plain-text versions read \"Floor area (m2)\" and \"CO2 emissions\", which is what a search or an export sees. In the sheet the 2s are raised and lowered. Footnote markers like the ¹ after \"Revenue\" pair well with a note below the table.",[184,1583,1585],{"id":1584},"step-4-read-rich-text-back-and-keep-it","Step 4 — Read rich text back, and keep it",[10,1587,1588,1589,1592,1593,1596],{},"By default ",[28,1590,1591],{},"load_workbook"," returns rich text cells as plain strings. That is convenient for reading data, and dangerous for editing: save the workbook and the plain strings are written back, ",[14,1594,1595],{},"removing the formatting",". Compare the two ways of loading:",[189,1598,1600],{"className":240,"code":1599,"language":242,"meta":194,"style":194},"plain = load_workbook(\"invoices_rich.xlsx\").active[\"B2\"].value\nrich = load_workbook(\"invoices_rich.xlsx\", rich_text=True).active[\"B2\"].value\nprint(type(plain).__name__, repr(plain))\nprint(type(rich).__name__, [(type(p).__name__, str(p)) for p in rich])\n\nload_workbook(\"invoices_rich.xlsx\").save(\"flattened.xlsx\")\nprint(\"after a plain load and save:\", type(load_workbook(\"flattened.xlsx\", rich_text=True).active[\"B2\"].value).__name__)\n",[28,1601,1602,1622,1647,1668,1707,1711,1726],{"__ignoreMap":194},[198,1603,1604,1607,1609,1611,1613,1616,1619],{"class":98,"line":200},[198,1605,1606],{"class":253},"plain ",[198,1608,309],{"class":249},[198,1610,942],{"class":253},[198,1612,920],{"class":207},[198,1614,1615],{"class":253},").active[",[198,1617,1618],{"class":207},"\"B2\"",[198,1620,1621],{"class":253},"].value\n",[198,1623,1624,1627,1629,1631,1633,1635,1637,1639,1641,1643,1645],{"class":98,"line":263},[198,1625,1626],{"class":253},"rich ",[198,1628,309],{"class":249},[198,1630,942],{"class":253},[198,1632,920],{"class":207},[198,1634,316],{"class":253},[198,1636,949],{"class":334},[198,1638,309],{"class":249},[198,1640,407],{"class":312},[198,1642,1615],{"class":253},[198,1644,1618],{"class":207},[198,1646,1621],{"class":253},[198,1648,1649,1651,1653,1655,1658,1660,1662,1665],{"class":98,"line":276},[198,1650,527],{"class":312},[198,1652,530],{"class":253},[198,1654,533],{"class":312},[198,1656,1657],{"class":253},"(plain).",[198,1659,544],{"class":312},[198,1661,316],{"class":253},[198,1663,1664],{"class":312},"repr",[198,1666,1667],{"class":253},"(plain))\n",[198,1669,1670,1672,1674,1676,1679,1681,1684,1686,1689,1691,1693,1695,1698,1700,1702,1704],{"class":98,"line":289},[198,1671,527],{"class":312},[198,1673,530],{"class":253},[198,1675,533],{"class":312},[198,1677,1678],{"class":253},"(rich).",[198,1680,544],{"class":312},[198,1682,1683],{"class":253},", [(",[198,1685,533],{"class":312},[198,1687,1688],{"class":253},"(p).",[198,1690,544],{"class":312},[198,1692,316],{"class":253},[198,1694,554],{"class":312},[198,1696,1697],{"class":253},"(p)) ",[198,1699,877],{"class":249},[198,1701,874],{"class":253},[198,1703,882],{"class":249},[198,1705,1706],{"class":253}," rich])\n",[198,1708,1709],{"class":98,"line":296},[198,1710,293],{"emptyLinePlaceholder":292},[198,1712,1713,1716,1718,1721,1724],{"class":98,"line":325},[198,1714,1715],{"class":253},"load_workbook(",[198,1717,920],{"class":207},[198,1719,1720],{"class":253},").save(",[198,1722,1723],{"class":207},"\"flattened.xlsx\"",[198,1725,431],{"class":253},[198,1727,1728,1730,1732,1735,1737,1739,1742,1744,1746,1748,1750,1752,1754,1756,1758,1760],{"class":98,"line":358},[198,1729,527],{"class":312},[198,1731,530],{"class":253},[198,1733,1734],{"class":207},"\"after a plain load and save:\"",[198,1736,316],{"class":253},[198,1738,533],{"class":312},[198,1740,1741],{"class":253},"(load_workbook(",[198,1743,1723],{"class":207},[198,1745,316],{"class":253},[198,1747,949],{"class":334},[198,1749,309],{"class":249},[198,1751,407],{"class":312},[198,1753,1615],{"class":253},[198,1755,1618],{"class":207},[198,1757,541],{"class":253},[198,1759,544],{"class":312},[198,1761,431],{"class":253},[10,1763,1764,1765,1767,1768,1770,1771,1773,1774,1777,1778,1781,1782,1784,1785,1777,1787,1790,1791,1793],{},"With ",[28,1766,1208],{},", the value is a ",[28,1769,91],{}," whose parts can be inspected — each ",[28,1772,233],{}," exposes ",[28,1775,1776],{},".font"," and ",[28,1779,1780],{},".text"," — and edited. Without it, the value is a ",[28,1783,554],{},", and the round trip through ",[28,1786,1591],{},[28,1788,1789],{},"save"," produces a file where the status cell is plain text. When a script opens a template or a report that anyone might have formatted by hand, load it with ",[28,1792,1208],{}," if it will be saved.",[38,1795,47,1801,47,1804,47,1807,47,1811,47,1815,47,1822,47,1828,47,1832,47,1838,47,1845,47,1849,47,1854,47,1858,47,1863,47,1866,47,1872,47,1877,47,1881,47,1885,47,1890,47,1893,47,1898,47,1901],{"viewBox":1796,"role":41,"ariaLabelledBy":1797,"xmlns":45,"style":1800},"4 0 752 207",[1798,1799],"rt-load-t","rt-load-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[49,1802,1803],{"id":1798},"Loading a workbook with and without rich_text=True",[53,1805,1806],{"id":1799},"Two panels. With the default load, rich text cells become plain strings, so saving the workbook writes plain text and the bold red word loses its formatting. With rich_text=True, cells load as CellRichText with their parts and fonts, and saving keeps the formatting.",[57,1808],{"x":1047,"y":60,"width":1809,"height":1810,"fill":63},"752","207",[57,1812],{"x":1813,"y":67,"width":1814,"height":1364,"rx":461,"fill":110,"stroke":111,"style":73},"20.0","351.0",[75,1816,1818],{"transform":1817},"translate(182.5,29.0) scale(1.083)",[79,1819],{"d":1820,"fill":82,"stroke":118,"style":1821},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[86,1823,1827],{"x":1824,"y":1825,"style":1826},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","load_workbook(path)",[98,1829],{"x1":1830,"y1":1281,"x2":1831,"y2":1281,"stroke":111,"style":1239},"36.0","355.0",[86,1833,1837],{"x":1834,"y":1835,"style":1836},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","cells read as str",[75,1839,1841],{"transform":1840},"translate(38.5,99.5) scale(0.625)",[79,1842],{"d":1843,"fill":82,"stroke":111,"style":1844},"M6 6l12 12 M18 6L6 18","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[86,1846,1848],{"x":1834,"y":1847,"style":1836},"137.0","save writes plain text",[75,1850,1852],{"transform":1851},"translate(38.5,125.5) scale(0.625)",[79,1853],{"d":1843,"fill":82,"stroke":111,"style":1844},[86,1855,1857],{"x":1834,"y":1856,"style":1836},"163.0","formatting silently lost",[75,1859,1861],{"transform":1860},"translate(38.5,151.5) scale(0.625)",[79,1862],{"d":1843,"fill":82,"stroke":111,"style":1844},[57,1864],{"x":1865,"y":67,"width":1814,"height":1364,"rx":461,"fill":165,"stroke":166,"style":73},"389.0",[75,1867,1869],{"transform":1868},"translate(551.5,29.0) scale(1.083)",[79,1870],{"d":1871,"fill":82,"stroke":173,"style":1821},"M5 12.5l4.5 4.5L19 7",[86,1873,1876],{"x":1874,"y":1825,"style":1875},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","load_workbook(path, rich_text=True)",[98,1878],{"x1":1879,"y1":1281,"x2":1880,"y2":1281,"stroke":166,"style":1239},"405.0","724.0",[86,1882,1884],{"x":1883,"y":1835,"style":1836},"429.0","cells read as CellRichText",[75,1886,1888],{"transform":1887},"translate(407.5,99.5) scale(0.625)",[79,1889],{"d":1871,"fill":82,"stroke":166,"style":1844},[86,1891,1892],{"x":1883,"y":1847,"style":1836},"parts and fonts available",[75,1894,1896],{"transform":1895},"translate(407.5,125.5) scale(0.625)",[79,1897],{"d":1871,"fill":82,"stroke":166,"style":1844},[86,1899,1900],{"x":1883,"y":1856,"style":1836},"save keeps formatting",[75,1902,1904],{"transform":1903},"translate(407.5,151.5) scale(0.625)",[79,1905],{"d":1871,"fill":82,"stroke":166,"style":1844},[184,1907,1909],{"id":1908},"limits-worth-knowing","Limits worth knowing",[10,1911,1912,1913,1916],{},"Rich text applies to text values only. A formula's result cannot be partly bold — for a formula cell, use a normal cell font or conditional formatting instead. Cell-level fonts still apply to the plain-string parts, so a cell with ",[28,1914,1915],{},"Font(size=9)"," shows its plain parts at 9 points while TextBlocks use their own sizes; set both consistently. And a very long cell with many runs is hard to edit by hand in Excel; keep rich text for short labels and highlights, not whole paragraphs of formatted prose.",[184,1918,1920],{"id":1919},"common-pitfalls","Common pitfalls",[1922,1923,1924,1933,1943,1952,1958,1964],"ul",{},[1925,1926,1927,1932],"li",{},[14,1928,1929,1930,36],{},"Saving without ",[28,1931,1208],{}," A plain load flattens every rich text cell on save.",[1925,1934,1935,1938,1939,1777,1941,36],{},[14,1936,1937],{},"Inline fonts without a name and size."," Runs fall back to the application default; set ",[28,1940,335],{},[28,1942,345],{},[1925,1944,1945,1948,1949,36],{},[14,1946,1947],{},"Colours without alpha."," Use ARGB strings such as ",[28,1950,1951],{},"FFC00000",[1925,1953,1954,1957],{},[14,1955,1956],{},"Formatting formula results."," Rich text is for text constants only.",[1925,1959,1960,1963],{},[14,1961,1962],{},"Expecting pandas to write it."," Write with pandas, then replace cells with openpyxl.",[1925,1965,1966,1969,1970,1972],{},[14,1967,1968],{},"Old openpyxl versions."," ",[28,1971,91],{}," needs openpyxl 3.1 or later.",[184,1974,1976],{"id":1975},"performance-notes","Performance notes",[10,1978,1979,1980,1982,1983,1985,1986,1988],{},"Building a ",[28,1981,91],{}," is cheap, but each run adds XML to the shared strings; thousands of rich cells are fine, millions will make the file large and slow to open. Create each ",[28,1984,178],{}," once and reuse it, as the ",[28,1987,647],{}," mapping does with colours.",[184,1990,1992],{"id":1991},"conclusion","Conclusion",[10,1994,1995,1996,1999,2000,2002,2003,2005,2006,2008,2009,2012],{},"Build rich text from plain strings and ",[28,1997,1998],{},"TextBlock(InlineFont(...), text)"," parts, and assign the ",[28,2001,91],{}," to a cell. Generate it from data — status colours from a mapping, keywords split out with ",[28,2004,601],{}," — and use ",[28,2007,591],{}," for subscripts and superscripts. Give inline fonts an explicit name and size, read existing rich text with ",[28,2010,2011],{},"load_workbook(..., rich_text=True)",", and always load that way before saving a workbook that contains rich text.",[184,2014,2016],{"id":2015},"frequently-asked-questions","Frequently asked questions",[10,2018,2019,2022],{},[14,2020,2021],{},"Can openpyxl write text with different formats in one cell?","\nYes, from openpyxl 3.1. Build a CellRichText from plain strings and TextBlock objects, where each TextBlock pairs an InlineFont — bold, italic, colour, size, font name — with the text it applies to, and assign it to the cell's value.",[10,2024,2025,2028],{},[14,2026,2027],{},"Why does my rich text disappear when I open and save a workbook with openpyxl?","\nBy default load_workbook reads rich text cells as plain strings, and saving writes those plain strings back. Pass rich_text=True to load_workbook whenever you will save a workbook that contains rich text.",[10,2030,2031,2034],{},[14,2032,2033],{},"How do I write subscript or superscript text in Excel with Python?","\nUse an InlineFont with vertAlign=\"subscript\" or vertAlign=\"superscript\" for the characters that should be lowered or raised, such as the 2 in H2O or m2.",[10,2036,2037,2040],{},[14,2038,2039],{},"Can pandas write rich text to Excel?","\nNo. to_excel writes each value as a plain cell. Write the table with pandas, then open the file with openpyxl and replace the cells that need mixed formatting with CellRichText values — or use xlsxwriter's write_rich_string while writing.",[184,2042,2044],{"id":2043},"related","Related",[1922,2046,2047,2053,2058,2065],{},[1925,2048,2049,2050,2052],{},"Up one level: ",[19,2051,22],{"href":21}," — whole-cell fonts, fills and borders.",[1925,2054,2055,2057],{},[19,2056,35],{"href":34}," — the xlsxwriter equivalent.",[1925,2059,2060,2064],{},[19,2061,2063],{"href":2062},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl\u002F","Highlight Cells Containing Specific Text with openpyxl"," — whole-cell highlighting by rule.",[1925,2066,2067,2071],{},[19,2068,2070],{"href":2069},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel Template Without Losing Formatting"," — other things a load and save can lose.",[2073,2074,2075],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":194,"searchDepth":263,"depth":263,"links":2077},[2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088],{"id":186,"depth":263,"text":187},{"id":223,"depth":263,"text":224},{"id":594,"depth":263,"text":595},{"id":1405,"depth":263,"text":1406},{"id":1584,"depth":263,"text":1585},{"id":1908,"depth":263,"text":1909},{"id":1919,"depth":263,"text":1920},{"id":1975,"depth":263,"text":1976},{"id":1991,"depth":263,"text":1992},{"id":2015,"depth":263,"text":2016},{"id":2043,"depth":263,"text":2044},"2026-09-25","Put more than one format in a single cell — a red, bold OVERDUE inside a status line, a highlighted keyword in a comment, H₂O and m² with real subscripts and superscripts. Build CellRichText from TextBlock and InlineFont objects, generate it from data, read existing rich text back, and avoid the load-and-save that silently flattens it.","md",[2093,2095,2097,2099],{"q":2021,"a":2094},"Yes, from openpyxl 3.1. Build a CellRichText from plain strings and TextBlock objects, where each TextBlock pairs an InlineFont — bold, italic, colour, size, font name — with the text it applies to, and assign it to the cell's value.",{"q":2027,"a":2096},"By default load_workbook reads rich text cells as plain strings, and saving writes those plain strings back. Pass rich_text=True to load_workbook whenever you will save a workbook that contains rich text.",{"q":2033,"a":2098},"Use an InlineFont with vertAlign=\"subscript\" or vertAlign=\"superscript\" for the characters that should be lowered or raised, such as the 2 in H2O or m2.",{"q":2039,"a":2100},"No. to_excel writes each value as a plain cell. Write the table with pandas, then open the file with openpyxl and replace the cells that need mixed formatting with CellRichText values — or use xlsxwriter's write_rich_string while writing.","write rich text in a cell with openpyxl",null,{"breadcrumb":2104},[2105,2108,2111],{"name":2106,"item":2107},"Home","\u002F",{"name":2109,"item":2110},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":22,"item":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl",{"title":2114,"description":2115},"Rich Text with Mixed Formatting in One Cell (openpyxl)","Write rich text in Excel cells with openpyxl 3.1: CellRichText, TextBlock and InlineFont, bold and coloured words, highlighting search terms, subscript and superscript, reading rich text with rich_text=True, and why saving without it removes the formatting.","write-rich-text-in-a-cell-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrite-rich-text-in-a-cell-with-openpyxl\u002Findex",[2119,2120],"cell-styling","openpyxl","how-to","FMd6piHuPzgNNU5xPPuWjABJQiVBudbyDLGCy8hPDWA",[2124,2128],{"title":2125,"path":2126,"stem":2127,"children":-1},"Wrap Text and Align Cells with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrap-text-and-align-cells-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrap-text-and-align-cells-with-openpyxl\u002Findex",{"title":2129,"path":2130,"stem":2131,"children":-1},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation","getting-started-with-python-excel-automation\u002Findex",[2133,2135],{"path":2134,"title":2109},"\u002Fformatting-and-charting-excel-reports-with-python",{"path":2136,"title":22},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl",[],[2139,2141,2144,2147,2150,2153],{"path":2140,"title":35,"kind":2121},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-rich-text-in-a-cell-with-xlsxwriter",{"path":2142,"title":2143,"kind":2121},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fcopy-cell-styles-with-openpyxl","Copy Cell Styles from One Cell to Another with openpyxl",{"path":2145,"title":2146,"kind":2121},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fauto-fit-column-widths-when-writing-with-pandas","Auto-Fit Column Widths When Writing with pandas",{"path":2148,"title":2149,"kind":2121},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fexport-a-styled-pandas-dataframe-to-excel","Export a Styled pandas DataFrame to Excel",{"path":2151,"title":2152,"kind":2121},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fgenerate-a-staff-rota-in-excel-with-python","Generate a Staff Rota in Excel with Python",{"path":2154,"title":2155,"kind":2121},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",1790972118820]