[{"data":1,"prerenderedAt":2243},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl":2209,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl":2218,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl":2223,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl":2224},{"id":4,"title":5,"body":6,"dateModified":2174,"datePublished":2174,"description":2175,"extension":2176,"faq":2177,"keyword":2186,"kind":2187,"meta":2188,"navigation":309,"path":2197,"seo":2198,"slug":2201,"stem":2202,"topics":2203,"type":2207,"__hash__":2208},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl\u002Findex.md","Highlight Cells Containing Specific Text with openpyxl",{"type":7,"value":8,"toc":2159},"minimark",[9,24,212,217,242,392,398,402,409,522,537,541,558,625,639,643,646,711,722,994,998,1005,1173,1186,1190,1200,1368,1391,1395,1398,1496,1504,1629,1633,1640,1982,1995,1999,2053,2057,2067,2071,2094,2098,2104,2110,2116,2122,2126,2155],[10,11,12,13,17,18,23],"p",{},"Status columns, comment fields and reference codes carry meaning in their text: \"late\", \"on hold\", \"cancelled\", a reference that starts with ",[14,15,16],"code",{},"CRN"," for a credit note. A conditional format that colours those cells turns a column of words into something a reader can scan in a second. openpyxl can write Excel's text rules, but they have one requirement that is easy to miss — a formula the spreadsheet actually evaluates — and one behaviour that surprises almost everyone: \"contains late\" also matches \"calculate\". This page, part of ",[19,20,22],"a",{"href":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","Applying Conditional Formatting With openpyxl",", writes each kind of text rule, and every result below was checked by rendering the workbook with LibreOffice.",[25,26,34,35,34,39,34,43,34,51,34,58,34,67,34,71,34,77,34,81,34,85,34,89,34,94,34,96,34,100,34,102,34,105,34,108,34,112,34,114,34,118,34,120,34,123,34,126,34,130,34,132,34,135,34,140,34,144,34,147,34,151,34,153,34,156,34,158,34,161,34,164,34,168,34,170,34,173,34,175,34,178,34,181,34,185,34,187,34,190,34,192,34,195,34,198,34,202,34,204,34,207,34,209],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"217 0 325 284","img",[30,31],"txt-result-t","txt-result-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:325px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"Status cells that mention late or delayed, highlighted",[40,41,42],"desc",{"id":31},"A shipments sheet with reference and status columns. A whole-word rule for the words late and delayed highlights Late, late - chase and Delayed at customs. It does not highlight calculate, which contains the letters l-a-t-e inside another word, or on time and cancelled.",[44,45],"rect",{"x":46,"y":47,"width":48,"height":49,"fill":50},"217","0","325","284","#ffffff",[52,53,57],"text",{"x":54,"y":55,"style":56},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Whole-word match for 'late' or 'delayed'",[44,59],{"x":60,"y":61,"width":62,"height":63,"rx":47,"fill":64,"stroke":65,"style":66},"246.3","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[44,68],{"x":69,"y":61,"width":70,"height":63,"rx":47,"fill":64,"stroke":65,"style":66},"278.3","89.9",[52,72,76],{"x":73,"y":74,"style":75},"323.3","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[44,78],{"x":79,"y":61,"width":80,"height":63,"rx":47,"fill":64,"stroke":65,"style":66},"368.2","145.5",[52,82,84],{"x":83,"y":74,"style":75},"441.0","B",[44,86],{"x":60,"y":87,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"72.0","28.0",[52,90,93],{"x":91,"y":92,"style":75},"262.3","91.0","1",[44,95],{"x":69,"y":87,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,97,99],{"x":73,"y":92,"style":98},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","ref",[44,101],{"x":79,"y":87,"width":80,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,103,104],{"x":83,"y":92,"style":98},"status",[44,106],{"x":60,"y":107,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"100.0",[52,109,111],{"x":91,"y":110,"style":75},"119.0","2",[44,113],{"x":69,"y":107,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,115,117],{"x":73,"y":110,"style":116},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","INV-1041",[44,119],{"x":79,"y":107,"width":80,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,121,122],{"x":83,"y":110,"style":116},"on time",[44,124],{"x":60,"y":125,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"128.0",[52,127,129],{"x":91,"y":128,"style":75},"147.0","3",[44,131],{"x":69,"y":125,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,133,134],{"x":73,"y":128,"style":116},"INV-1042",[44,136],{"x":79,"y":125,"width":80,"height":88,"rx":47,"fill":137,"stroke":138,"style":139},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[52,141,143],{"x":83,"y":128,"style":142},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Late",[44,145],{"x":60,"y":146,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"156.0",[52,148,150],{"x":91,"y":149,"style":75},"175.0","4",[44,152],{"x":69,"y":146,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,154,155],{"x":73,"y":149,"style":116},"CRN-0007",[44,157],{"x":79,"y":146,"width":80,"height":88,"rx":47,"fill":137,"stroke":138,"style":139},[52,159,160],{"x":83,"y":149,"style":142},"late - chase",[44,162],{"x":60,"y":163,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"184.0",[52,165,167],{"x":91,"y":166,"style":75},"203.0","5",[44,169],{"x":69,"y":163,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,171,172],{"x":73,"y":166,"style":116},"INV-1050",[44,174],{"x":79,"y":163,"width":80,"height":88,"rx":47,"fill":137,"stroke":138,"style":139},[52,176,177],{"x":83,"y":166,"style":142},"Delayed at customs",[44,179],{"x":60,"y":180,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"212.0",[52,182,184],{"x":91,"y":183,"style":75},"231.0","6",[44,186],{"x":69,"y":180,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,188,189],{"x":73,"y":183,"style":116},"PO-3",[44,191],{"x":79,"y":180,"width":80,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,193,194],{"x":83,"y":183,"style":116},"cancelled",[44,196],{"x":60,"y":197,"width":62,"height":88,"rx":47,"fill":64,"stroke":65,"style":66},"240.0",[52,199,201],{"x":91,"y":200,"style":75},"259.0","7",[44,203],{"x":69,"y":197,"width":70,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,205,206],{"x":73,"y":200,"style":116},"INV-1060",[44,208],{"x":79,"y":197,"width":80,"height":88,"rx":47,"fill":50,"stroke":65,"style":66},[52,210,211],{"x":83,"y":200,"style":116},"calculate",[213,214,216],"h2",{"id":215},"prerequisites","Prerequisites",[218,219,224],"pre",{"className":220,"code":221,"language":222,"meta":223,"style":223},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[14,225,226],{"__ignoreMap":223},[227,228,231,235,239],"span",{"class":229,"line":230},"line",1,[227,232,234],{"class":233},"sMTad","pip",[227,236,238],{"class":237},"srMev"," install",[227,240,241],{"class":237}," openpyxl\n",[218,243,247],{"className":244,"code":245,"language":246,"meta":223,"style":223},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\nfrom openpyxl.formatting.rule import Rule, FormulaRule\nfrom openpyxl.styles import PatternFill, Font\nfrom openpyxl.styles.differential import DifferentialStyle\n\nAMBER = PatternFill(\"solid\", bgColor=\"FFEB9C\")\nFLAG = DifferentialStyle(fill=AMBER, font=Font(color=\"9C5700\", bold=True))\n","python",[14,248,249,265,278,291,304,311,343],{"__ignoreMap":223},[227,250,251,255,259,262],{"class":229,"line":230},[227,252,254],{"class":253},"s-kum","from",[227,256,258],{"class":257},"skGVy"," openpyxl ",[227,260,261],{"class":253},"import",[227,263,264],{"class":257}," load_workbook\n",[227,266,268,270,273,275],{"class":229,"line":267},2,[227,269,254],{"class":253},[227,271,272],{"class":257}," openpyxl.formatting.rule ",[227,274,261],{"class":253},[227,276,277],{"class":257}," Rule, FormulaRule\n",[227,279,281,283,286,288],{"class":229,"line":280},3,[227,282,254],{"class":253},[227,284,285],{"class":257}," openpyxl.styles ",[227,287,261],{"class":253},[227,289,290],{"class":257}," PatternFill, Font\n",[227,292,294,296,299,301],{"class":229,"line":293},4,[227,295,254],{"class":253},[227,297,298],{"class":257}," openpyxl.styles.differential ",[227,300,261],{"class":253},[227,302,303],{"class":257}," DifferentialStyle\n",[227,305,307],{"class":229,"line":306},5,[227,308,310],{"emptyLinePlaceholder":309},true,"\n",[227,312,314,318,321,324,327,330,334,337,340],{"class":229,"line":313},6,[227,315,317],{"class":316},"sP0c6","AMBER",[227,319,320],{"class":253}," =",[227,322,323],{"class":257}," PatternFill(",[227,325,326],{"class":237},"\"solid\"",[227,328,329],{"class":257},", ",[227,331,333],{"class":332},"sa561","bgColor",[227,335,336],{"class":253},"=",[227,338,339],{"class":237},"\"FFEB9C\"",[227,341,342],{"class":257},")\n",[227,344,346,349,351,354,357,359,361,363,366,368,371,374,376,379,381,384,386,389],{"class":229,"line":345},7,[227,347,348],{"class":316},"FLAG",[227,350,320],{"class":253},[227,352,353],{"class":257}," DifferentialStyle(",[227,355,356],{"class":332},"fill",[227,358,336],{"class":253},[227,360,317],{"class":316},[227,362,329],{"class":257},[227,364,365],{"class":332},"font",[227,367,336],{"class":253},[227,369,370],{"class":257},"Font(",[227,372,373],{"class":332},"color",[227,375,336],{"class":253},[227,377,378],{"class":237},"\"9C5700\"",[227,380,329],{"class":257},[227,382,383],{"class":332},"bold",[227,385,336],{"class":253},[227,387,388],{"class":316},"True",[227,390,391],{"class":257},"))\n",[10,393,394,397],{},[14,395,396],{},"shipments.xlsx"," has six shipments with references in column A and free-text statuses in column B, including one — \"calculate\" — planted to show the substring trap.",[213,399,401],{"id":400},"step-1-write-a-containstext-rule-with-its-formula","Step 1 — Write a containsText rule with its formula",[10,403,404,405,408],{},"Excel's \"Text that contains\" rule is stored as a ",[14,406,407],{},"containsText"," rule. openpyxl needs three things: the type and operator, the text (which Excel shows in its rule dialog), and a formula that does the actual test, written for the top-left cell of the range:",[218,410,412],{"className":244,"code":411,"language":246,"meta":223,"style":223},"wb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws.conditional_formatting.add(\"B2:B7\", Rule(\n    type=\"containsText\", operator=\"containsText\", text=\"late\",\n    formula=['NOT(ISERROR(SEARCH(\"late\",B2)))'], dxf=FLAG))\nwb.save(\"contains.xlsx\")\n",[14,413,414,429,445,456,487,512],{"__ignoreMap":223},[227,415,416,419,421,424,427],{"class":229,"line":230},[227,417,418],{"class":257},"wb ",[227,420,336],{"class":253},[227,422,423],{"class":257}," load_workbook(",[227,425,426],{"class":237},"\"shipments.xlsx\"",[227,428,342],{"class":257},[227,430,431,434,436,439,442],{"class":229,"line":267},[227,432,433],{"class":257},"ws ",[227,435,336],{"class":253},[227,437,438],{"class":257}," wb[",[227,440,441],{"class":237},"\"Shipments\"",[227,443,444],{"class":257},"]\n",[227,446,447,450,453],{"class":229,"line":280},[227,448,449],{"class":257},"ws.conditional_formatting.add(",[227,451,452],{"class":237},"\"B2:B7\"",[227,454,455],{"class":257},", Rule(\n",[227,457,458,461,463,466,468,471,473,475,477,479,481,484],{"class":229,"line":293},[227,459,460],{"class":332},"    type",[227,462,336],{"class":253},[227,464,465],{"class":237},"\"containsText\"",[227,467,329],{"class":257},[227,469,470],{"class":332},"operator",[227,472,336],{"class":253},[227,474,465],{"class":237},[227,476,329],{"class":257},[227,478,52],{"class":332},[227,480,336],{"class":253},[227,482,483],{"class":237},"\"late\"",[227,485,486],{"class":257},",\n",[227,488,489,492,494,497,500,503,506,508,510],{"class":229,"line":306},[227,490,491],{"class":332},"    formula",[227,493,336],{"class":253},[227,495,496],{"class":257},"[",[227,498,499],{"class":237},"'NOT(ISERROR(SEARCH(\"late\",B2)))'",[227,501,502],{"class":257},"], ",[227,504,505],{"class":332},"dxf",[227,507,336],{"class":253},[227,509,348],{"class":316},[227,511,391],{"class":257},[227,513,514,517,520],{"class":229,"line":313},[227,515,516],{"class":257},"wb.save(",[227,518,519],{"class":237},"\"contains.xlsx\"",[227,521,342],{"class":257},[10,523,524,525,528,529,532,533,536],{},"The formula is the part that counts. The spreadsheet evaluates it for each cell, adjusting ",[14,526,527],{},"B2"," down the range; leave it out and the rule exists but never fires. ",[14,530,531],{},"SEARCH"," returns the position of the text or an error when it is absent, so ",[14,534,535],{},"NOT(ISERROR(...))"," is true whenever \"late\" appears anywhere. Rendered, the rule marks \"Late\", \"late - chase\" — and \"calculate\".",[213,538,540],{"id":539},"step-2-choose-case-insensitive-or-case-sensitive","Step 2 — Choose case-insensitive or case-sensitive",[10,542,543,545,546,549,550,553,554,557],{},[14,544,531],{}," ignores case, which is why \"Late\" matched. That is usually right for statuses people type. When case carries meaning — a code where ",[14,547,548],{},"ABC"," and ",[14,551,552],{},"abc"," differ — use ",[14,555,556],{},"FIND",", which is case-sensitive, in a plain formula rule:",[218,559,561],{"className":244,"code":560,"language":246,"meta":223,"style":223},"wb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws.conditional_formatting.add(\"B2:B7\", FormulaRule(formula=['ISNUMBER(FIND(\"Late\",B2))'], fill=AMBER))\nwb.save(\"case_sensitive.xlsx\")\n",[14,562,563,575,587,616],{"__ignoreMap":223},[227,564,565,567,569,571,573],{"class":229,"line":230},[227,566,418],{"class":257},[227,568,336],{"class":253},[227,570,423],{"class":257},[227,572,426],{"class":237},[227,574,342],{"class":257},[227,576,577,579,581,583,585],{"class":229,"line":267},[227,578,433],{"class":257},[227,580,336],{"class":253},[227,582,438],{"class":257},[227,584,441],{"class":237},[227,586,444],{"class":257},[227,588,589,591,593,596,599,601,603,606,608,610,612,614],{"class":229,"line":280},[227,590,449],{"class":257},[227,592,452],{"class":237},[227,594,595],{"class":257},", FormulaRule(",[227,597,598],{"class":332},"formula",[227,600,336],{"class":253},[227,602,496],{"class":257},[227,604,605],{"class":237},"'ISNUMBER(FIND(\"Late\",B2))'",[227,607,502],{"class":257},[227,609,356],{"class":332},[227,611,336],{"class":253},[227,613,317],{"class":316},[227,615,391],{"class":257},[227,617,618,620,623],{"class":229,"line":293},[227,619,516],{"class":257},[227,621,622],{"class":237},"\"case_sensitive.xlsx\"",[227,624,342],{"class":257},[10,626,627,628,549,631,634,635,638],{},"Only the capitalised \"Late\" is marked now. ",[14,629,630],{},"ISNUMBER(FIND(...))",[14,632,633],{},"NOT(ISERROR(SEARCH(...)))"," are interchangeable patterns; ",[14,636,637],{},"ISNUMBER"," reads a little more directly.",[213,640,642],{"id":641},"step-3-match-whole-words","Step 3 — Match whole words",[10,644,645],{},"\"calculate\" contains the letters l-a-t-e, so a plain contains rule marks it. For words, pad both the cell and the keyword with spaces: a whole word is surrounded by spaces in the padded text, and a fragment inside another word is not:",[218,647,649],{"className":244,"code":648,"language":246,"meta":223,"style":223},"wb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws.conditional_formatting.add(\"B2:B7\", FormulaRule(formula=['ISNUMBER(SEARCH(\" late \",\" \"&B2&\" \"))'], fill=AMBER))\nwb.save(\"whole_word.xlsx\")\n",[14,650,651,663,675,702],{"__ignoreMap":223},[227,652,653,655,657,659,661],{"class":229,"line":230},[227,654,418],{"class":257},[227,656,336],{"class":253},[227,658,423],{"class":257},[227,660,426],{"class":237},[227,662,342],{"class":257},[227,664,665,667,669,671,673],{"class":229,"line":267},[227,666,433],{"class":257},[227,668,336],{"class":253},[227,670,438],{"class":257},[227,672,441],{"class":237},[227,674,444],{"class":257},[227,676,677,679,681,683,685,687,689,692,694,696,698,700],{"class":229,"line":280},[227,678,449],{"class":257},[227,680,452],{"class":237},[227,682,595],{"class":257},[227,684,598],{"class":332},[227,686,336],{"class":253},[227,688,496],{"class":257},[227,690,691],{"class":237},"'ISNUMBER(SEARCH(\" late \",\" \"&B2&\" \"))'",[227,693,502],{"class":257},[227,695,356],{"class":332},[227,697,336],{"class":253},[227,699,317],{"class":316},[227,701,391],{"class":257},[227,703,704,706,709],{"class":229,"line":293},[227,705,516],{"class":257},[227,707,708],{"class":237},"\"whole_word.xlsx\"",[227,710,342],{"class":257},[10,712,713,714,717,718,721],{},"The padded text of \"late - chase\" is ",[14,715,716],{},"\" late - chase \"",", which contains ",[14,719,720],{},"\" late \"","; the padded \"calculate\" does not. The trick treats spaces as the only word boundary, so \"late,\" with a comma still matches only if a space follows the comma. For punctuation-heavy text, substitute punctuation with spaces inside the formula, or flag the rows in pandas with a regular expression and format on the flag.",[25,723,34,729,34,732,34,735,34,739,34,745,34,752,34,758,34,761,34,765,34,768,34,772,34,775,34,779,34,783,34,787,34,789,34,800,34,802,34,807,34,809,34,814,34,816,34,821,34,824,34,827,34,829,34,837,34,839,34,844,34,846,34,851,34,853,34,858,34,861,34,864,34,866,34,871,34,873,34,878,34,880,34,885,34,887,34,892,34,895,34,898,34,900,34,905,34,907,34,912,34,914,34,919,34,921,34,926,34,929,34,932,34,934,34,939,34,941,34,946,34,948,34,953,34,955,34,960,34,963,34,966,34,968,34,973,34,975,34,980,34,982,34,987,34,989],{"viewBox":724,"role":28,"ariaLabelledBy":725,"xmlns":32,"style":728},"4 0 752 276",[726,727],"txt-modes-t","txt-modes-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,730,731],{"id":726},"Which statuses each text rule highlights",[40,733,734],{"id":727},"A grid of the six statuses against four rules. Contains late with SEARCH marks Late, late - chase and calculate. Case-sensitive FIND for Late marks only Late. The whole-word rule marks Late and late - chase but not calculate. The whole-word rule for late or delayed also marks Delayed at customs. None of them mark on time or cancelled.",[44,736],{"x":150,"y":47,"width":737,"height":738,"fill":50},"752","276",[44,740],{"x":741,"y":742,"width":743,"height":744,"rx":47,"fill":50,"stroke":65,"style":66},"20.0","16.0","154.6","40.0",[44,746],{"x":747,"y":742,"width":748,"height":744,"rx":47,"fill":749,"stroke":750,"style":751},"174.6","141.4","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[52,753,757],{"x":754,"y":755,"style":756},"245.2","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","SEARCH \"late\"",[44,759],{"x":760,"y":742,"width":748,"height":744,"rx":47,"fill":749,"stroke":750,"style":751},"315.9",[52,762,764],{"x":763,"y":755,"style":756},"386.6","FIND \"Late\"",[44,766],{"x":767,"y":742,"width":748,"height":744,"rx":47,"fill":749,"stroke":750,"style":751},"457.3",[52,769,771],{"x":770,"y":755,"style":756},"528.0","whole word \"late\"",[44,773],{"x":774,"y":742,"width":748,"height":744,"rx":47,"fill":749,"stroke":750,"style":751},"598.6",[52,776,778],{"x":777,"y":755,"style":756},"669.3","late or delayed",[44,780],{"x":741,"y":781,"width":743,"height":782,"rx":47,"fill":64,"stroke":65,"style":66},"56.0","34.0",[52,784,122],{"x":62,"y":785,"style":786},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[44,788],{"x":747,"y":781,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,791,793],"g",{"transform":792},"translate(237.2,65.0) scale(0.667)",[794,795],"path",{"d":796,"fill":797,"stroke":798,"style":799},"M6 6l12 12 M18 6L6 18","none","var(--accent,#f43f8f)","stroke-width:2.70px;stroke-linecap:round;stroke-linejoin:round",[44,801],{"x":760,"y":781,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,803,805],{"transform":804},"translate(378.6,65.0) scale(0.667)",[794,806],{"d":796,"fill":797,"stroke":798,"style":799},[44,808],{"x":767,"y":781,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,810,812],{"transform":811},"translate(520.0,65.0) scale(0.667)",[794,813],{"d":796,"fill":797,"stroke":798,"style":799},[44,815],{"x":774,"y":781,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,817,819],{"transform":818},"translate(661.3,65.0) scale(0.667)",[794,820],{"d":796,"fill":797,"stroke":798,"style":799},[44,822],{"x":741,"y":823,"width":743,"height":782,"rx":47,"fill":64,"stroke":65,"style":66},"90.0",[52,825,143],{"x":62,"y":826,"style":786},"112.0",[44,828],{"x":747,"y":823,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,830,832],{"transform":831},"translate(236.2,98.0) scale(0.750)",[794,833],{"d":834,"fill":797,"stroke":835,"style":836},"M5 12.5l4.5 4.5L19 7","var(--teal,#0f9488)","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[44,838],{"x":760,"y":823,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,840,842],{"transform":841},"translate(377.6,98.0) scale(0.750)",[794,843],{"d":834,"fill":797,"stroke":835,"style":836},[44,845],{"x":767,"y":823,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,847,849],{"transform":848},"translate(519.0,98.0) scale(0.750)",[794,850],{"d":834,"fill":797,"stroke":835,"style":836},[44,852],{"x":774,"y":823,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,854,856],{"transform":855},"translate(660.3,98.0) scale(0.750)",[794,857],{"d":834,"fill":797,"stroke":835,"style":836},[44,859],{"x":741,"y":860,"width":743,"height":782,"rx":47,"fill":64,"stroke":65,"style":66},"124.0",[52,862,160],{"x":62,"y":863,"style":786},"146.0",[44,865],{"x":747,"y":860,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,867,869],{"transform":868},"translate(236.2,132.0) scale(0.750)",[794,870],{"d":834,"fill":797,"stroke":835,"style":836},[44,872],{"x":760,"y":860,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,874,876],{"transform":875},"translate(378.6,133.0) scale(0.667)",[794,877],{"d":796,"fill":797,"stroke":798,"style":799},[44,879],{"x":767,"y":860,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,881,883],{"transform":882},"translate(519.0,132.0) scale(0.750)",[794,884],{"d":834,"fill":797,"stroke":835,"style":836},[44,886],{"x":774,"y":860,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,888,890],{"transform":889},"translate(660.3,132.0) scale(0.750)",[794,891],{"d":834,"fill":797,"stroke":835,"style":836},[44,893],{"x":741,"y":894,"width":743,"height":782,"rx":47,"fill":64,"stroke":65,"style":66},"158.0",[52,896,177],{"x":62,"y":897,"style":786},"180.0",[44,899],{"x":747,"y":894,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,901,903],{"transform":902},"translate(237.2,167.0) scale(0.667)",[794,904],{"d":796,"fill":797,"stroke":798,"style":799},[44,906],{"x":760,"y":894,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,908,910],{"transform":909},"translate(378.6,167.0) scale(0.667)",[794,911],{"d":796,"fill":797,"stroke":798,"style":799},[44,913],{"x":767,"y":894,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,915,917],{"transform":916},"translate(520.0,167.0) scale(0.667)",[794,918],{"d":796,"fill":797,"stroke":798,"style":799},[44,920],{"x":774,"y":894,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,922,924],{"transform":923},"translate(660.3,166.0) scale(0.750)",[794,925],{"d":834,"fill":797,"stroke":835,"style":836},[44,927],{"x":741,"y":928,"width":743,"height":782,"rx":47,"fill":64,"stroke":65,"style":66},"192.0",[52,930,194],{"x":62,"y":931,"style":786},"214.0",[44,933],{"x":747,"y":928,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,935,937],{"transform":936},"translate(237.2,201.0) scale(0.667)",[794,938],{"d":796,"fill":797,"stroke":798,"style":799},[44,940],{"x":760,"y":928,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,942,944],{"transform":943},"translate(378.6,201.0) scale(0.667)",[794,945],{"d":796,"fill":797,"stroke":798,"style":799},[44,947],{"x":767,"y":928,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,949,951],{"transform":950},"translate(520.0,201.0) scale(0.667)",[794,952],{"d":796,"fill":797,"stroke":798,"style":799},[44,954],{"x":774,"y":928,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,956,958],{"transform":957},"translate(661.3,201.0) scale(0.667)",[794,959],{"d":796,"fill":797,"stroke":798,"style":799},[44,961],{"x":741,"y":962,"width":743,"height":782,"rx":47,"fill":64,"stroke":65,"style":66},"226.0",[52,964,211],{"x":62,"y":965,"style":786},"248.0",[44,967],{"x":747,"y":962,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,969,971],{"transform":970},"translate(236.2,234.0) scale(0.750)",[794,972],{"d":834,"fill":797,"stroke":835,"style":836},[44,974],{"x":760,"y":962,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,976,978],{"transform":977},"translate(378.6,235.0) scale(0.667)",[794,979],{"d":796,"fill":797,"stroke":798,"style":799},[44,981],{"x":767,"y":962,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,983,985],{"transform":984},"translate(520.0,235.0) scale(0.667)",[794,986],{"d":796,"fill":797,"stroke":798,"style":799},[44,988],{"x":774,"y":962,"width":748,"height":782,"rx":47,"fill":50,"stroke":65,"style":66},[790,990,992],{"transform":991},"translate(661.3,235.0) scale(0.667)",[794,993],{"d":796,"fill":797,"stroke":798,"style":799},[213,995,997],{"id":996},"step-4-match-any-word-from-a-list","Step 4 — Match any word from a list",[10,999,1000,1001,1004],{},"A status column usually has several words that mean trouble. Build an ",[14,1002,1003],{},"OR"," of one test per word in Python, so the list lives in your code or configuration rather than in a hand-edited formula:",[218,1006,1008],{"className":244,"code":1007,"language":246,"meta":223,"style":223},"TROUBLE = [\"late\", \"delayed\", \"on hold\"]\ntests = \",\".join(f'ISNUMBER(SEARCH(\" {w} \",\" \"&B2&\" \"))' for w in TROUBLE)\nformula = f\"OR({tests})\"\nprint(formula)\n\nwb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws.conditional_formatting.add(\"B2:B7\", FormulaRule(formula=[formula], fill=AMBER))\nwb.save(\"any_word.xlsx\")\n",[14,1009,1010,1034,1080,1103,1111,1115,1127,1139,1163],{"__ignoreMap":223},[227,1011,1012,1015,1017,1020,1022,1024,1027,1029,1032],{"class":229,"line":230},[227,1013,1014],{"class":316},"TROUBLE",[227,1016,320],{"class":253},[227,1018,1019],{"class":257}," [",[227,1021,483],{"class":237},[227,1023,329],{"class":257},[227,1025,1026],{"class":237},"\"delayed\"",[227,1028,329],{"class":257},[227,1030,1031],{"class":237},"\"on hold\"",[227,1033,444],{"class":257},[227,1035,1036,1039,1041,1044,1047,1050,1053,1057,1060,1063,1066,1069,1072,1075,1078],{"class":229,"line":267},[227,1037,1038],{"class":257},"tests ",[227,1040,336],{"class":253},[227,1042,1043],{"class":237}," \",\"",[227,1045,1046],{"class":257},".join(",[227,1048,1049],{"class":253},"f",[227,1051,1052],{"class":237},"'ISNUMBER(SEARCH(\" ",[227,1054,1056],{"class":1055},"sSjpA","{",[227,1058,1059],{"class":257},"w",[227,1061,1062],{"class":1055},"}",[227,1064,1065],{"class":237}," \",\" \"&B2&\" \"))'",[227,1067,1068],{"class":253}," for",[227,1070,1071],{"class":257}," w ",[227,1073,1074],{"class":253},"in",[227,1076,1077],{"class":316}," TROUBLE",[227,1079,342],{"class":257},[227,1081,1082,1085,1087,1090,1093,1095,1098,1100],{"class":229,"line":280},[227,1083,1084],{"class":257},"formula ",[227,1086,336],{"class":253},[227,1088,1089],{"class":253}," f",[227,1091,1092],{"class":237},"\"OR(",[227,1094,1056],{"class":1055},[227,1096,1097],{"class":257},"tests",[227,1099,1062],{"class":1055},[227,1101,1102],{"class":237},")\"\n",[227,1104,1105,1108],{"class":229,"line":293},[227,1106,1107],{"class":316},"print",[227,1109,1110],{"class":257},"(formula)\n",[227,1112,1113],{"class":229,"line":306},[227,1114,310],{"emptyLinePlaceholder":309},[227,1116,1117,1119,1121,1123,1125],{"class":229,"line":313},[227,1118,418],{"class":257},[227,1120,336],{"class":253},[227,1122,423],{"class":257},[227,1124,426],{"class":237},[227,1126,342],{"class":257},[227,1128,1129,1131,1133,1135,1137],{"class":229,"line":345},[227,1130,433],{"class":257},[227,1132,336],{"class":253},[227,1134,438],{"class":257},[227,1136,441],{"class":237},[227,1138,444],{"class":257},[227,1140,1142,1144,1146,1148,1150,1152,1155,1157,1159,1161],{"class":229,"line":1141},8,[227,1143,449],{"class":257},[227,1145,452],{"class":237},[227,1147,595],{"class":257},[227,1149,598],{"class":332},[227,1151,336],{"class":253},[227,1153,1154],{"class":257},"[formula], ",[227,1156,356],{"class":332},[227,1158,336],{"class":253},[227,1160,317],{"class":316},[227,1162,391],{"class":257},[227,1164,1166,1168,1171],{"class":229,"line":1165},9,[227,1167,516],{"class":257},[227,1169,1170],{"class":237},"\"any_word.xlsx\"",[227,1172,342],{"class":257},[10,1174,1175,1176,1178,1179,1182,1183,1185],{},"Spelling out one ",[14,1177,531],{}," per word is deliberate. The shorter form with an array constant, ",[14,1180,1181],{},"OR(ISNUMBER(SEARCH({\"late\",\"delayed\"},B2)))",", did not evaluate as an array inside a conditional format in testing — it matched only the first keyword — while the explicit ",[14,1184,1003],{}," matched all three statuses. Multi-word phrases such as \"on hold\" work with the same padding, as long as the words in the cell are separated by single spaces. Escape any double quote in a keyword by doubling it before building the formula.",[213,1187,1189],{"id":1188},"step-5-match-the-start-or-end-of-the-text","Step 5 — Match the start or end of the text",[10,1191,1192,1193,549,1196,1199],{},"References and codes are better matched by position than by content. ",[14,1194,1195],{},"beginsWith",[14,1197,1198],{},"endsWith"," rules exist for this, and again carry their own formula:",[218,1201,1203],{"className":244,"code":1202,"language":246,"meta":223,"style":223},"wb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws.conditional_formatting.add(\"A2:A7\", Rule(\n    type=\"beginsWith\", operator=\"beginsWith\", text=\"CRN\",\n    formula=['LEFT(A2,LEN(\"CRN\"))=\"CRN\"'], dxf=FLAG))\nws.conditional_formatting.add(\"B2:B7\", Rule(\n    type=\"notContainsText\", operator=\"notContains\", text=\"late\",\n    formula=['ISERROR(SEARCH(\"late\",B2))'], dxf=DifferentialStyle(font=Font(color=\"808080\"))))\nwb.save(\"starts_with.xlsx\")\n",[14,1204,1205,1217,1229,1238,1266,1287,1295,1323,1359],{"__ignoreMap":223},[227,1206,1207,1209,1211,1213,1215],{"class":229,"line":230},[227,1208,418],{"class":257},[227,1210,336],{"class":253},[227,1212,423],{"class":257},[227,1214,426],{"class":237},[227,1216,342],{"class":257},[227,1218,1219,1221,1223,1225,1227],{"class":229,"line":267},[227,1220,433],{"class":257},[227,1222,336],{"class":253},[227,1224,438],{"class":257},[227,1226,441],{"class":237},[227,1228,444],{"class":257},[227,1230,1231,1233,1236],{"class":229,"line":280},[227,1232,449],{"class":257},[227,1234,1235],{"class":237},"\"A2:A7\"",[227,1237,455],{"class":257},[227,1239,1240,1242,1244,1247,1249,1251,1253,1255,1257,1259,1261,1264],{"class":229,"line":293},[227,1241,460],{"class":332},[227,1243,336],{"class":253},[227,1245,1246],{"class":237},"\"beginsWith\"",[227,1248,329],{"class":257},[227,1250,470],{"class":332},[227,1252,336],{"class":253},[227,1254,1246],{"class":237},[227,1256,329],{"class":257},[227,1258,52],{"class":332},[227,1260,336],{"class":253},[227,1262,1263],{"class":237},"\"CRN\"",[227,1265,486],{"class":257},[227,1267,1268,1270,1272,1274,1277,1279,1281,1283,1285],{"class":229,"line":306},[227,1269,491],{"class":332},[227,1271,336],{"class":253},[227,1273,496],{"class":257},[227,1275,1276],{"class":237},"'LEFT(A2,LEN(\"CRN\"))=\"CRN\"'",[227,1278,502],{"class":257},[227,1280,505],{"class":332},[227,1282,336],{"class":253},[227,1284,348],{"class":316},[227,1286,391],{"class":257},[227,1288,1289,1291,1293],{"class":229,"line":313},[227,1290,449],{"class":257},[227,1292,452],{"class":237},[227,1294,455],{"class":257},[227,1296,1297,1299,1301,1304,1306,1308,1310,1313,1315,1317,1319,1321],{"class":229,"line":345},[227,1298,460],{"class":332},[227,1300,336],{"class":253},[227,1302,1303],{"class":237},"\"notContainsText\"",[227,1305,329],{"class":257},[227,1307,470],{"class":332},[227,1309,336],{"class":253},[227,1311,1312],{"class":237},"\"notContains\"",[227,1314,329],{"class":257},[227,1316,52],{"class":332},[227,1318,336],{"class":253},[227,1320,483],{"class":237},[227,1322,486],{"class":257},[227,1324,1325,1327,1329,1331,1334,1336,1338,1340,1343,1345,1347,1349,1351,1353,1356],{"class":229,"line":1141},[227,1326,491],{"class":332},[227,1328,336],{"class":253},[227,1330,496],{"class":257},[227,1332,1333],{"class":237},"'ISERROR(SEARCH(\"late\",B2))'",[227,1335,502],{"class":257},[227,1337,505],{"class":332},[227,1339,336],{"class":253},[227,1341,1342],{"class":257},"DifferentialStyle(",[227,1344,365],{"class":332},[227,1346,336],{"class":253},[227,1348,370],{"class":257},[227,1350,373],{"class":332},[227,1352,336],{"class":253},[227,1354,1355],{"class":237},"\"808080\"",[227,1357,1358],{"class":257},"))))\n",[227,1360,1361,1363,1366],{"class":229,"line":1165},[227,1362,516],{"class":257},[227,1364,1365],{"class":237},"\"starts_with.xlsx\"",[227,1367,342],{"class":257},[10,1369,1370,1371,1373,1374,1378,1379,1381,1382,1385,1386,1390],{},"The first rule marks credit notes (",[14,1372,155],{},") in the reference column; the second greys out every status that does ",[1375,1376,1377],"em",{},"not"," mention \"late\", which draws the eye to the ones that do by making the rest quieter. For ",[14,1380,1198],{},", the formula is ",[14,1383,1384],{},"RIGHT(A2,LEN(\"X\"))=\"X\"",". ",[19,1387,1389],{"href":1388},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-an-entire-row-based-on-a-cell-value-with-openpyxl\u002F","Highlight an Entire Row Based on a Cell Value with openpyxl"," shows how to colour the whole shipment row instead of one cell.",[213,1392,1394],{"id":1393},"step-6-let-readers-choose-the-keyword","Step 6 — Let readers choose the keyword",[10,1396,1397],{},"A small search box — a keyword typed into D1 that highlights matching statuses — makes a report interactive without any code on the reader's side. Refer to the cell absolutely, and guard against an empty box, because an empty search string matches everything:",[218,1399,1401],{"className":244,"code":1400,"language":246,"meta":223,"style":223},"wb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws[\"D1\"] = \"late\"\nws[\"C1\"] = \"Highlight:\"\nws.conditional_formatting.add(\"B2:B7\", FormulaRule(\n    formula=['AND($D$1\u003C>\"\",ISNUMBER(SEARCH($D$1,B2)))'], fill=AMBER))\nwb.save(\"search_box.xlsx\")\n",[14,1402,1403,1415,1427,1443,1457,1466,1487],{"__ignoreMap":223},[227,1404,1405,1407,1409,1411,1413],{"class":229,"line":230},[227,1406,418],{"class":257},[227,1408,336],{"class":253},[227,1410,423],{"class":257},[227,1412,426],{"class":237},[227,1414,342],{"class":257},[227,1416,1417,1419,1421,1423,1425],{"class":229,"line":267},[227,1418,433],{"class":257},[227,1420,336],{"class":253},[227,1422,438],{"class":257},[227,1424,441],{"class":237},[227,1426,444],{"class":257},[227,1428,1429,1432,1435,1438,1440],{"class":229,"line":280},[227,1430,1431],{"class":257},"ws[",[227,1433,1434],{"class":237},"\"D1\"",[227,1436,1437],{"class":257},"] ",[227,1439,336],{"class":253},[227,1441,1442],{"class":237}," \"late\"\n",[227,1444,1445,1447,1450,1452,1454],{"class":229,"line":293},[227,1446,1431],{"class":257},[227,1448,1449],{"class":237},"\"C1\"",[227,1451,1437],{"class":257},[227,1453,336],{"class":253},[227,1455,1456],{"class":237}," \"Highlight:\"\n",[227,1458,1459,1461,1463],{"class":229,"line":306},[227,1460,449],{"class":257},[227,1462,452],{"class":237},[227,1464,1465],{"class":257},", FormulaRule(\n",[227,1467,1468,1470,1472,1474,1477,1479,1481,1483,1485],{"class":229,"line":313},[227,1469,491],{"class":332},[227,1471,336],{"class":253},[227,1473,496],{"class":257},[227,1475,1476],{"class":237},"'AND($D$1\u003C>\"\",ISNUMBER(SEARCH($D$1,B2)))'",[227,1478,502],{"class":257},[227,1480,356],{"class":332},[227,1482,336],{"class":253},[227,1484,317],{"class":316},[227,1486,391],{"class":257},[227,1488,1489,1491,1494],{"class":229,"line":345},[227,1490,516],{"class":257},[227,1492,1493],{"class":237},"\"search_box.xlsx\"",[227,1495,342],{"class":257},[10,1497,1498,1499,1503],{},"With \"late\" in D1, the matching statuses light up; clear D1 and the highlighting disappears instead of covering the column. Pair it with a dropdown of common keywords — ",[19,1500,1502],{"href":1501},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fadd-dropdown-data-validation-to-excel-with-openpyxl\u002F","Add Dropdown Data Validation to Excel with openpyxl"," — and readers can switch between views with one click.",[25,1505,34,1510,34,1513,34,1516,34,1519,34,1525,34,1533,34,1539,34,1544,34,1549,34,1554,34,1557,34,1564,34,1569,34,1572,34,1576,34,1579,34,1582,34,1588,34,1593,34,1597,34,1601,34,1605,34,1608,34,1612,34,1618,34,1623,34,1626],{"viewBox":1506,"role":28,"ariaLabelledBy":1507,"xmlns":32,"style":728},"4 0 752 142",[1508,1509],"txt-eval-t","txt-eval-d",[36,1511,1512],{"id":1508},"How a text rule decides whether to colour a cell",[40,1514,1515],{"id":1509},"Four steps. The spreadsheet takes the rule formula written for the top-left cell. It adjusts relative references for each cell in the range, so B2 becomes B3, B4 and so on. It evaluates SEARCH or FIND against that cell's text. If the result is true, the rule's style is applied over the cell's own formatting.",[44,1517],{"x":150,"y":47,"width":737,"height":1518,"fill":50},"142",[44,1520],{"x":741,"y":742,"width":1521,"height":1522,"rx":1523,"fill":64,"stroke":65,"style":1524},"153.0","110.5","12","stroke-width:2px",[790,1526,1528],{"transform":1527},"translate(84.5,37.8) scale(1.000)",[794,1529],{"d":1530,"fill":797,"stroke":1531,"style":1532},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[52,1534,1538],{"x":1535,"y":1536,"style":1537},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Rule formula",[52,1540,1543],{"x":1535,"y":1541,"style":1542},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","written for B2",[229,1545],{"x1":1546,"y1":1547,"x2":1548,"y2":1547,"stroke":750,"style":1524},"178.0","71.2","200.0",[1550,1551],"polygon",{"points":1552,"fill":1553},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[44,1555],{"x":1556,"y":742,"width":1521,"height":1522,"rx":1523,"fill":749,"stroke":750,"style":1524},"209.0",[790,1558,1560],{"transform":1559},"translate(273.5,37.8) scale(1.000)",[794,1561],{"d":1562,"fill":797,"stroke":1563,"style":1532},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--brand-strong,#4338ca)",[52,1565,1568],{"x":1566,"y":1536,"style":1567},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Adjust per cell",[52,1570,1571],{"x":1566,"y":1541,"style":1542},"B2 → B3 → B4 …",[229,1573],{"x1":1574,"y1":1547,"x2":1575,"y2":1547,"stroke":750,"style":1524},"367.0","389.0",[1550,1577],{"points":1578,"fill":1553},"393.0,71.2 384.0,76.2 384.0,66.2",[44,1580],{"x":1581,"y":742,"width":1521,"height":1522,"rx":1523,"fill":749,"stroke":750,"style":1524},"398.0",[790,1583,1585],{"transform":1584},"translate(462.5,29.0) scale(1.000)",[794,1586],{"d":1587,"fill":797,"stroke":1563,"style":1532},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[52,1589,1592],{"x":1590,"y":1591,"style":1567},"474.5","75.5","Evaluate",[52,1594,1596],{"x":1590,"y":1595,"style":1542},"95.0","SEARCH \u002F FIND on",[52,1598,1600],{"x":1590,"y":1599,"style":1542},"112.5","that text",[229,1602],{"x1":1603,"y1":1547,"x2":1604,"y2":1547,"stroke":750,"style":1524},"556.0","578.0",[1550,1606],{"points":1607,"fill":1553},"582.0,71.2 573.0,76.2 573.0,66.2",[44,1609],{"x":1610,"y":742,"width":1521,"height":1522,"rx":1523,"fill":1611,"stroke":835,"style":1524},"587.0","#d9f4f1",[790,1613,1615],{"transform":1614},"translate(651.5,29.0) scale(1.000)",[794,1616],{"d":834,"fill":797,"stroke":1617,"style":1532},"var(--teal-ink,#0b6157)",[52,1619,1622],{"x":1620,"y":1591,"style":1621},"663.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Apply style",[52,1624,1625],{"x":1620,"y":1595,"style":1542},"when the result is",[52,1627,1628],{"x":1620,"y":1599,"style":1542},"TRUE",[213,1630,1632],{"id":1631},"apply-one-rule-to-several-columns","Apply one rule to several columns",[10,1634,1635,1636,1639],{},"Real sheets often have the same keyword problem in several places — a status column, a notes column, a free-text reason on another sheet. Each range needs a formula anchored on ",[1375,1637,1638],{},"its own"," top-left cell, which is easy to get wrong when copying rules by hand. A helper that derives the anchor from the range keeps it right:",[218,1641,1643],{"className":244,"code":1642,"language":246,"meta":223,"style":223},"from openpyxl.utils.cell import range_boundaries\nfrom openpyxl.utils import get_column_letter\n\ndef highlight_words(ws, cell_range, words, fill=AMBER):\n    min_col, min_row, _, _ = range_boundaries(cell_range)\n    anchor = f\"{get_column_letter(min_col)}{min_row}\"\n    tests = \",\".join(f'ISNUMBER(SEARCH(\" {w.replace(chr(34), chr(34) * 2)} \",\" \"&{anchor}&\" \"))' for w in words)\n    ws.conditional_formatting.add(cell_range, FormulaRule(formula=[f\"OR({tests})\"], fill=fill))\n    return anchor\n\nwb = load_workbook(\"shipments.xlsx\")\nws = wb[\"Shipments\"]\nws[\"C1\"] = \"note\"\nws[\"C3\"] = \"customer says delayed\"\nprint(highlight_words(ws, \"B2:B7\", TROUBLE), highlight_words(ws, \"C2:C7\", TROUBLE))\nwb.save(\"several_columns.xlsx\")\nprint([(str(cf.sqref), cf.rules[0].formula[0][:32]) for cf in ws.conditional_formatting])\n",[14,1644,1645,1657,1669,1673,1692,1702,1730,1804,1837,1845,1850,1863,1876,1890,1905,1931,1941],{"__ignoreMap":223},[227,1646,1647,1649,1652,1654],{"class":229,"line":230},[227,1648,254],{"class":253},[227,1650,1651],{"class":257}," openpyxl.utils.cell ",[227,1653,261],{"class":253},[227,1655,1656],{"class":257}," range_boundaries\n",[227,1658,1659,1661,1664,1666],{"class":229,"line":267},[227,1660,254],{"class":253},[227,1662,1663],{"class":257}," openpyxl.utils ",[227,1665,261],{"class":253},[227,1667,1668],{"class":257}," get_column_letter\n",[227,1670,1671],{"class":229,"line":280},[227,1672,310],{"emptyLinePlaceholder":309},[227,1674,1675,1678,1682,1685,1687,1689],{"class":229,"line":293},[227,1676,1677],{"class":253},"def",[227,1679,1681],{"class":1680},"s_Opv"," highlight_words",[227,1683,1684],{"class":257},"(ws, cell_range, words, fill",[227,1686,336],{"class":253},[227,1688,317],{"class":316},[227,1690,1691],{"class":257},"):\n",[227,1693,1694,1697,1699],{"class":229,"line":306},[227,1695,1696],{"class":257},"    min_col, min_row, _, _ ",[227,1698,336],{"class":253},[227,1700,1701],{"class":257}," range_boundaries(cell_range)\n",[227,1703,1704,1707,1709,1711,1714,1716,1719,1722,1725,1727],{"class":229,"line":313},[227,1705,1706],{"class":257},"    anchor ",[227,1708,336],{"class":253},[227,1710,1089],{"class":253},[227,1712,1713],{"class":237},"\"",[227,1715,1056],{"class":1055},[227,1717,1718],{"class":257},"get_column_letter(min_col)",[227,1720,1721],{"class":1055},"}{",[227,1723,1724],{"class":257},"min_row",[227,1726,1062],{"class":1055},[227,1728,1729],{"class":237},"\"\n",[227,1731,1732,1735,1737,1739,1741,1743,1745,1747,1750,1753,1756,1759,1762,1764,1766,1768,1771,1774,1777,1780,1782,1785,1787,1790,1792,1795,1797,1799,1801],{"class":229,"line":345},[227,1733,1734],{"class":257},"    tests ",[227,1736,336],{"class":253},[227,1738,1043],{"class":237},[227,1740,1046],{"class":257},[227,1742,1049],{"class":253},[227,1744,1052],{"class":237},[227,1746,1056],{"class":1055},[227,1748,1749],{"class":257},"w.replace(",[227,1751,1752],{"class":316},"chr",[227,1754,1755],{"class":257},"(",[227,1757,1758],{"class":316},"34",[227,1760,1761],{"class":257},"), ",[227,1763,1752],{"class":316},[227,1765,1755],{"class":257},[227,1767,1758],{"class":316},[227,1769,1770],{"class":257},") ",[227,1772,1773],{"class":253},"*",[227,1775,1776],{"class":316}," 2",[227,1778,1779],{"class":257},")",[227,1781,1062],{"class":1055},[227,1783,1784],{"class":237}," \",\" \"&",[227,1786,1056],{"class":1055},[227,1788,1789],{"class":257},"anchor",[227,1791,1062],{"class":1055},[227,1793,1794],{"class":237},"&\" \"))'",[227,1796,1068],{"class":253},[227,1798,1071],{"class":257},[227,1800,1074],{"class":253},[227,1802,1803],{"class":257}," words)\n",[227,1805,1806,1809,1811,1813,1815,1817,1819,1821,1823,1825,1828,1830,1832,1834],{"class":229,"line":1141},[227,1807,1808],{"class":257},"    ws.conditional_formatting.add(cell_range, FormulaRule(",[227,1810,598],{"class":332},[227,1812,336],{"class":253},[227,1814,496],{"class":257},[227,1816,1049],{"class":253},[227,1818,1092],{"class":237},[227,1820,1056],{"class":1055},[227,1822,1097],{"class":257},[227,1824,1062],{"class":1055},[227,1826,1827],{"class":237},")\"",[227,1829,502],{"class":257},[227,1831,356],{"class":332},[227,1833,336],{"class":253},[227,1835,1836],{"class":257},"fill))\n",[227,1838,1839,1842],{"class":229,"line":1165},[227,1840,1841],{"class":253},"    return",[227,1843,1844],{"class":257}," anchor\n",[227,1846,1848],{"class":229,"line":1847},10,[227,1849,310],{"emptyLinePlaceholder":309},[227,1851,1853,1855,1857,1859,1861],{"class":229,"line":1852},11,[227,1854,418],{"class":257},[227,1856,336],{"class":253},[227,1858,423],{"class":257},[227,1860,426],{"class":237},[227,1862,342],{"class":257},[227,1864,1866,1868,1870,1872,1874],{"class":229,"line":1865},12,[227,1867,433],{"class":257},[227,1869,336],{"class":253},[227,1871,438],{"class":257},[227,1873,441],{"class":237},[227,1875,444],{"class":257},[227,1877,1879,1881,1883,1885,1887],{"class":229,"line":1878},13,[227,1880,1431],{"class":257},[227,1882,1449],{"class":237},[227,1884,1437],{"class":257},[227,1886,336],{"class":253},[227,1888,1889],{"class":237}," \"note\"\n",[227,1891,1893,1895,1898,1900,1902],{"class":229,"line":1892},14,[227,1894,1431],{"class":257},[227,1896,1897],{"class":237},"\"C3\"",[227,1899,1437],{"class":257},[227,1901,336],{"class":253},[227,1903,1904],{"class":237}," \"customer says delayed\"\n",[227,1906,1908,1910,1913,1915,1917,1919,1922,1925,1927,1929],{"class":229,"line":1907},15,[227,1909,1107],{"class":316},[227,1911,1912],{"class":257},"(highlight_words(ws, ",[227,1914,452],{"class":237},[227,1916,329],{"class":257},[227,1918,1014],{"class":316},[227,1920,1921],{"class":257},"), highlight_words(ws, ",[227,1923,1924],{"class":237},"\"C2:C7\"",[227,1926,329],{"class":257},[227,1928,1014],{"class":316},[227,1930,391],{"class":257},[227,1932,1934,1936,1939],{"class":229,"line":1933},16,[227,1935,516],{"class":257},[227,1937,1938],{"class":237},"\"several_columns.xlsx\"",[227,1940,342],{"class":257},[227,1942,1944,1946,1949,1952,1955,1957,1960,1962,1965,1968,1971,1974,1977,1979],{"class":229,"line":1943},17,[227,1945,1107],{"class":316},[227,1947,1948],{"class":257},"([(",[227,1950,1951],{"class":316},"str",[227,1953,1954],{"class":257},"(cf.sqref), cf.rules[",[227,1956,47],{"class":316},[227,1958,1959],{"class":257},"].formula[",[227,1961,47],{"class":316},[227,1963,1964],{"class":257},"][:",[227,1966,1967],{"class":316},"32",[227,1969,1970],{"class":257},"]) ",[227,1972,1973],{"class":253},"for",[227,1975,1976],{"class":257}," cf ",[227,1978,1074],{"class":253},[227,1980,1981],{"class":257}," ws.conditional_formatting])\n",[10,1983,1984,1985,1987,1988,1991,1992,1994],{},"Each range gets its own anchor — ",[14,1986,527],{}," for the status column, ",[14,1989,1990],{},"C2"," for the notes — so every cell tests itself rather than the column next to it. Doubling any ",[14,1993,1713],{}," in a keyword keeps the formula valid if the list ever contains a quoted phrase.",[213,1996,1998],{"id":1997},"common-pitfalls","Common pitfalls",[2000,2001,2002,2010,2016,2033,2043],"ul",{},[2003,2004,2005,2009],"li",{},[2006,2007,2008],"strong",{},"No formula on a text rule."," The rule is stored but never fires.",[2003,2011,2012,2015],{},[2006,2013,2014],{},"Substring matches."," \"late\" matches \"calculate\"; pad with spaces for whole words.",[2003,2017,2018,2021,2022,2025,2026,2028,2029,2032],{},[2006,2019,2020],{},"Absolute references in the tested cell."," ",[14,2023,2024],{},"$B$2"," makes every cell test B2; use ",[14,2027,527],{}," (or ",[14,2030,2031],{},"$B2"," for row rules).",[2003,2034,2035,2038,2039,2042],{},[2006,2036,2037],{},"Empty keyword cells."," Guard with ",[14,2040,2041],{},"$D$1\u003C>\"\"",", or an empty box highlights everything.",[2003,2044,2045,2048,2049,2052],{},[2006,2046,2047],{},"Array constants in rule formulas."," Build explicit ",[14,2050,2051],{},"OR(...)"," terms in Python instead.",[213,2054,2056],{"id":2055},"performance-notes","Performance notes",[10,2058,2059,2060,2062,2063,2066],{},"Text rules are evaluated per cell by the spreadsheet on every recalculation. A handful of ",[14,2061,531],{}," calls per cell is negligible for thousands of rows; for very long lists of keywords on large sheets, classify the rows in pandas with ",[14,2064,2065],{},"str.contains"," and a regular expression, write a category column, and use one simple rule on it.",[213,2068,2070],{"id":2069},"conclusion","Conclusion",[10,2072,2073,2074,329,2076,329,2079,2081,2082,2084,2085,2087,2088,2090,2091,2093],{},"Write text rules with openpyxl as ",[14,2075,407],{},[14,2077,2078],{},"notContainsText",[14,2080,1195],{}," or ",[14,2083,1198],{}," rules — always with a formula that does the test — or as plain formula rules. Use ",[14,2086,531],{}," for case-insensitive and ",[14,2089,556],{}," for case-sensitive matching, pad with spaces to match whole words, build ",[14,2092,1003],{}," expressions in Python for keyword lists, and point the formula at a cell to let readers choose the keyword. Render the workbook once to confirm the rule marks exactly the cells you meant.",[213,2095,2097],{"id":2096},"frequently-asked-questions","Frequently asked questions",[10,2099,2100,2103],{},[2006,2101,2102],{},"Why does my containsText rule highlight nothing?","\nopenpyxl writes the rule's type and text, but the spreadsheet evaluates its formula. Without a formula such as NOT(ISERROR(SEARCH(\"late\",B2))) the rule has nothing to test. Always pass the formula alongside the text.",[10,2105,2106,2109],{},[2006,2107,2108],{},"Is text matching case-sensitive?","\nSEARCH, which Excel's own \"Text that contains\" rule uses, ignores case. For a case-sensitive match, write a formula rule with FIND instead, for example ISNUMBER(FIND(\"Late\",B2)).",[10,2111,2112,2115],{},[2006,2113,2114],{},"How do I stop \"late\" from matching \"calculate\"?","\nMatch whole words by padding both sides with spaces: ISNUMBER(SEARCH(\" late \",\" \"&B2&\" \")). A word surrounded by spaces in the padded text is a whole word; a word inside another word is not.",[10,2117,2118,2121],{},[2006,2119,2120],{},"Can the keyword come from a cell?","\nYes. Refer to it with an absolute reference such as $D$1 in the formula, and guard against an empty keyword with AND($D$1\u003C>\"\", ...), otherwise every cell matches.",[213,2123,2125],{"id":2124},"related","Related",[2000,2127,2128,2134,2141,2148],{},[2003,2129,2130,2131,2133],{},"Up one level: ",[19,2132,22],{"href":21}," — every rule type.",[2003,2135,2136,2140],{},[19,2137,2139],{"href":2138},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-top-and-bottom-values-with-openpyxl\u002F","Highlight Top and Bottom Values in Excel with openpyxl"," — ranking rules and checking results without Excel.",[2003,2142,2143,2147],{},[19,2144,2146],{"href":2145},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Ffind-search-and-substitute-equivalents-in-pandas\u002F","Find Search and Substitute Equivalents in pandas"," — the same text functions in Python.",[2003,2149,2150,2154],{},[19,2151,2153],{"href":2152},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-blank-cells-with-openpyxl\u002F","Highlight Blank Cells in Excel with openpyxl"," — the empty-cell counterpart.",[2156,2157,2158],"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 .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}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":223,"searchDepth":267,"depth":267,"links":2160},[2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173],{"id":215,"depth":267,"text":216},{"id":400,"depth":267,"text":401},{"id":539,"depth":267,"text":540},{"id":641,"depth":267,"text":642},{"id":996,"depth":267,"text":997},{"id":1188,"depth":267,"text":1189},{"id":1393,"depth":267,"text":1394},{"id":1631,"depth":267,"text":1632},{"id":1997,"depth":267,"text":1998},{"id":2055,"depth":267,"text":2056},{"id":2069,"depth":267,"text":2070},{"id":2096,"depth":267,"text":2097},{"id":2124,"depth":267,"text":2125},"2026-09-25","Colour every status that mentions \"late\", every reference that begins with INV, or every note containing any word from a list. Write containsText, beginsWith and formula rules with openpyxl, choose between case-insensitive SEARCH and case-sensitive FIND, match whole words so \"late\" does not match \"calculate\", and drive the keyword from a cell.","md",[2178,2180,2182,2184],{"q":2102,"a":2179},"openpyxl writes the rule's type and text, but the spreadsheet evaluates its formula. Without a formula such as NOT(ISERROR(SEARCH(\"late\",B2))) the rule has nothing to test. Always pass the formula alongside the text.",{"q":2108,"a":2181},"SEARCH, which Excel's own \"Text that contains\" rule uses, ignores case. For a case-sensitive match, write a formula rule with FIND instead, for example ISNUMBER(FIND(\"Late\",B2)).",{"q":2114,"a":2183},"Match whole words by padding both sides with spaces: ISNUMBER(SEARCH(\" late \",\" \"&B2&\" \")). A word surrounded by spaces in the padded text is a whole word; a word inside another word is not.",{"q":2120,"a":2185},"Yes. Refer to it with an absolute reference such as $D$1 in the formula, and guard against an empty keyword with AND($D$1\u003C>\"\", ...), otherwise every cell matches.","highlight cells containing specific text with openpyxl",null,{"breadcrumb":2189},[2190,2193,2196],{"name":2191,"item":2192},"Home","\u002F",{"name":2194,"item":2195},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":22,"item":21},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl",{"title":2199,"description":2200},"openpyxl Conditional Formatting for Cells Containing Text","Highlight Excel cells that contain, begin with or do not contain text using openpyxl: containsText and beginsWith rules with their required formulas, case-sensitive matching, whole-word matching, several keywords and a keyword cell.","highlight-cells-containing-text-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-containing-text-with-openpyxl\u002Findex",[2204,2205,2206],"conditional-formatting","openpyxl","data-validation","how-to","kE79tqn7eSwX9VBRtrmm5v78DhI5rN1Va3vokE0nmog",[2210,2214],{"title":2211,"path":2212,"stem":2213,"children":-1},"Highlight Cells Above a Threshold with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-above-a-threshold-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-above-a-threshold-with-openpyxl\u002Findex",{"title":2215,"path":2216,"stem":2217,"children":-1},"Highlight Cells with Formula Errors Using Conditional Formatting","\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-with-formula-errors-using-conditional-formatting","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-with-formula-errors-using-conditional-formatting\u002Findex",[2219,2221],{"path":2220,"title":22},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl",{"path":2222,"title":2194},"\u002Fadvanced-data-transformation-and-cleaning",[],[2225,2228,2231,2234,2237,2240],{"path":2226,"title":2227,"kind":2207},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fapply-conditional-formatting-with-xlsxwriter","Apply Conditional Formatting with xlsxwriter",{"path":2229,"title":2230,"kind":2207},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-data-bars-and-colour-scales-with-openpyxl","Add Data Bars and Colour Scales with openpyxl",{"path":2232,"title":2233,"kind":2207},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-icon-sets-with-openpyxl","Add Icon Sets to Excel Cells with openpyxl",{"path":2235,"title":2236,"kind":2207},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-a-colour-scale-per-row-or-per-column-with-openpyxl","Apply a Colour Scale per Row or per Column with openpyxl",{"path":2238,"title":2239,"kind":2207},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows","Apply Conditional Formatting to an Excel Table That Grows",{"path":2241,"title":2242,"kind":2207},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fcontrol-conditional-formatting-rule-order-and-stop-if-true","Control Conditional Formatting Rule Order and Stop If True",1790972078206]