[{"data":1,"prerenderedAt":2308},["ShallowReactive",2],{"doc:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows":3,"surround:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows":2279,"ancestors:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows":2287,"children:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows":2292,"related:\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows":2293},{"id":4,"title":5,"body":6,"dateModified":2244,"datePublished":2244,"description":2245,"extension":2246,"faq":2247,"keyword":2256,"kind":2257,"meta":2258,"navigation":145,"path":2268,"seo":2269,"slug":2272,"stem":2273,"topics":2274,"type":2277,"__hash__":2278},"docs\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows\u002Findex.md","Apply Conditional Formatting to an Excel Table That Grows",{"type":7,"value":8,"toc":2232},"minimark",[9,19,24,50,58,62,65,743,758,762,765,1074,1081,1241,1245,1248,1468,1475,1608,1612,1619,1765,1777,1781,1792,2012,2019,2123,2127,2162,2166,2169,2173,2179,2185,2191,2197,2201,2228],[10,11,12,13,18],"p",{},"A weekly job appends new orders to a tracking workbook. The orders live in an Excel table, and conditional formatting highlights late deliveries and large orders. In week one everything looks right. By week four the newest rows are plain — the highlighting stopped at the row where the table ended when the rules were created. When a person adds rows inside a table in Excel, Excel usually extends the formatting for them; when a script appends rows and resizes the table, the rules' ranges stay exactly where they were. This page shows the problem with openpyxl, fixes it by re-pointing every rule at the table's current range after each append, compares that with the simpler trick of oversized ranges, explains why table references cannot be used inside rule formulas, and adds a check that no data row is left uncovered. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002F","Applying Conditional Formatting with openpyxl",".",[20,21,23],"h2",{"id":22},"prerequisites","Prerequisites",[25,26,31],"pre",{"className":27,"code":28,"language":29,"meta":30,"style":30},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[32,33,34],"code",{"__ignoreMap":30},[35,36,39,43,47],"span",{"class":37,"line":38},"line",1,[35,40,42],{"class":41},"sMTad","pip",[35,44,46],{"class":45},"srMev"," install",[35,48,49],{"class":45}," openpyxl\n",[10,51,52,53,57],{},"The examples create an orders workbook with an Excel table and two highlighting rules, then append a week of new orders the way a scheduled job would. Appending to a table is covered in its own right in ",[14,54,56],{"href":55},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fappend-rows-to-an-excel-table-and-resize-it\u002F","Append Rows to an Excel Table and Resize It","; this page is about keeping the formatting in step.",[20,59,61],{"id":60},"step-1-a-table-with-two-rules","Step 1 — A table with two rules",[10,63,64],{},"Create the workbook: an Orders table and two formula rules over its data rows — late deliveries in red, orders over 5,000 in bold amber:",[25,66,70],{"className":67,"code":68,"language":69,"meta":30,"style":30},"language-python shiki shiki-themes github-light github-dark-high-contrast","from datetime import date, timedelta\nfrom openpyxl import Workbook\nfrom openpyxl.formatting.rule import FormulaRule\nfrom openpyxl.styles import Font, PatternFill\nfrom openpyxl.worksheet.table import Table, TableStyleInfo\n\nwb = Workbook()\nws = wb.active\nws.title = \"Orders\"\nws.append([\"Order\", \"Due\", \"Delivered\", \"Amount\"])\nbase = date(2026, 9, 1)\nfor i in range(8):\n    due = base + timedelta(days=i)\n    ws.append([f\"SO-{1001 + i}\", due, due + timedelta(days=(i % 4) - 1), 1200 + i * 900])\nfor row in ws.iter_rows(min_row=2, min_col=2, max_col=3):\n    for c in row:\n        c.number_format = \"yyyy-mm-dd\"\n\nlast = ws.max_row\ntable = Table(displayName=\"Orders\", ref=f\"A1:D{last}\", tableStyleInfo=TableStyleInfo(name=\"TableStyleLight9\", showRowStripes=True))\nws.add_table(table)\nLATE = FormulaRule(formula=[\"$C2>$B2\"], fill=PatternFill(\"solid\", fgColor=\"FFC7CE\"), font=Font(color=\"9C0006\"))\nBIG = FormulaRule(formula=[\"$D2>5000\"], fill=PatternFill(\"solid\", fgColor=\"FFEB9C\"), font=Font(bold=True))\nws.conditional_formatting.add(f\"A2:D{last}\", LATE)\nws.conditional_formatting.add(f\"D2:D{last}\", BIG)\nwb.save(\"orders_tracker.xlsx\")\nprint(\"table\", table.ref, \"| rule ranges:\", [str(cf.sqref) for cf in ws.conditional_formatting])\n","python",[32,71,72,88,101,114,127,140,147,159,170,181,209,237,261,287,365,407,421,432,437,448,519,525,592,647,672,696,707],{"__ignoreMap":30},[35,73,74,78,82,85],{"class":37,"line":38},[35,75,77],{"class":76},"s-kum","from",[35,79,81],{"class":80},"skGVy"," datetime ",[35,83,84],{"class":76},"import",[35,86,87],{"class":80}," date, timedelta\n",[35,89,91,93,96,98],{"class":37,"line":90},2,[35,92,77],{"class":76},[35,94,95],{"class":80}," openpyxl ",[35,97,84],{"class":76},[35,99,100],{"class":80}," Workbook\n",[35,102,104,106,109,111],{"class":37,"line":103},3,[35,105,77],{"class":76},[35,107,108],{"class":80}," openpyxl.formatting.rule ",[35,110,84],{"class":76},[35,112,113],{"class":80}," FormulaRule\n",[35,115,117,119,122,124],{"class":37,"line":116},4,[35,118,77],{"class":76},[35,120,121],{"class":80}," openpyxl.styles ",[35,123,84],{"class":76},[35,125,126],{"class":80}," Font, PatternFill\n",[35,128,130,132,135,137],{"class":37,"line":129},5,[35,131,77],{"class":76},[35,133,134],{"class":80}," openpyxl.worksheet.table ",[35,136,84],{"class":76},[35,138,139],{"class":80}," Table, TableStyleInfo\n",[35,141,143],{"class":37,"line":142},6,[35,144,146],{"emptyLinePlaceholder":145},true,"\n",[35,148,150,153,156],{"class":37,"line":149},7,[35,151,152],{"class":80},"wb ",[35,154,155],{"class":76},"=",[35,157,158],{"class":80}," Workbook()\n",[35,160,162,165,167],{"class":37,"line":161},8,[35,163,164],{"class":80},"ws ",[35,166,155],{"class":76},[35,168,169],{"class":80}," wb.active\n",[35,171,173,176,178],{"class":37,"line":172},9,[35,174,175],{"class":80},"ws.title ",[35,177,155],{"class":76},[35,179,180],{"class":45}," \"Orders\"\n",[35,182,184,187,190,193,196,198,201,203,206],{"class":37,"line":183},10,[35,185,186],{"class":80},"ws.append([",[35,188,189],{"class":45},"\"Order\"",[35,191,192],{"class":80},", ",[35,194,195],{"class":45},"\"Due\"",[35,197,192],{"class":80},[35,199,200],{"class":45},"\"Delivered\"",[35,202,192],{"class":80},[35,204,205],{"class":45},"\"Amount\"",[35,207,208],{"class":80},"])\n",[35,210,212,215,217,220,224,226,229,231,234],{"class":37,"line":211},11,[35,213,214],{"class":80},"base ",[35,216,155],{"class":76},[35,218,219],{"class":80}," date(",[35,221,223],{"class":222},"sP0c6","2026",[35,225,192],{"class":80},[35,227,228],{"class":222},"9",[35,230,192],{"class":80},[35,232,233],{"class":222},"1",[35,235,236],{"class":80},")\n",[35,238,240,243,246,249,252,255,258],{"class":37,"line":239},12,[35,241,242],{"class":76},"for",[35,244,245],{"class":80}," i ",[35,247,248],{"class":76},"in",[35,250,251],{"class":222}," range",[35,253,254],{"class":80},"(",[35,256,257],{"class":222},"8",[35,259,260],{"class":80},"):\n",[35,262,264,267,269,272,275,278,282,284],{"class":37,"line":263},13,[35,265,266],{"class":80},"    due ",[35,268,155],{"class":76},[35,270,271],{"class":80}," base ",[35,273,274],{"class":76},"+",[35,276,277],{"class":80}," timedelta(",[35,279,281],{"class":280},"sa561","days",[35,283,155],{"class":76},[35,285,286],{"class":80},"i)\n",[35,288,290,293,296,299,303,306,309,312,315,318,321,323,325,327,329,332,335,338,341,344,347,350,353,355,357,360,363],{"class":37,"line":289},14,[35,291,292],{"class":80},"    ws.append([",[35,294,295],{"class":76},"f",[35,297,298],{"class":45},"\"SO-",[35,300,302],{"class":301},"sSjpA","{",[35,304,305],{"class":222},"1001",[35,307,308],{"class":76}," +",[35,310,311],{"class":80}," i",[35,313,314],{"class":301},"}",[35,316,317],{"class":45},"\"",[35,319,320],{"class":80},", due, due ",[35,322,274],{"class":76},[35,324,277],{"class":80},[35,326,281],{"class":280},[35,328,155],{"class":76},[35,330,331],{"class":80},"(i ",[35,333,334],{"class":76},"%",[35,336,337],{"class":222}," 4",[35,339,340],{"class":80},") ",[35,342,343],{"class":76},"-",[35,345,346],{"class":222}," 1",[35,348,349],{"class":80},"), ",[35,351,352],{"class":222},"1200",[35,354,308],{"class":76},[35,356,245],{"class":80},[35,358,359],{"class":76},"*",[35,361,362],{"class":222}," 900",[35,364,208],{"class":80},[35,366,368,370,373,375,378,381,383,386,388,391,393,395,397,400,402,405],{"class":37,"line":367},15,[35,369,242],{"class":76},[35,371,372],{"class":80}," row ",[35,374,248],{"class":76},[35,376,377],{"class":80}," ws.iter_rows(",[35,379,380],{"class":280},"min_row",[35,382,155],{"class":76},[35,384,385],{"class":222},"2",[35,387,192],{"class":80},[35,389,390],{"class":280},"min_col",[35,392,155],{"class":76},[35,394,385],{"class":222},[35,396,192],{"class":80},[35,398,399],{"class":280},"max_col",[35,401,155],{"class":76},[35,403,404],{"class":222},"3",[35,406,260],{"class":80},[35,408,410,413,416,418],{"class":37,"line":409},16,[35,411,412],{"class":76},"    for",[35,414,415],{"class":80}," c ",[35,417,248],{"class":76},[35,419,420],{"class":80}," row:\n",[35,422,424,427,429],{"class":37,"line":423},17,[35,425,426],{"class":80},"        c.number_format ",[35,428,155],{"class":76},[35,430,431],{"class":45}," \"yyyy-mm-dd\"\n",[35,433,435],{"class":37,"line":434},18,[35,436,146],{"emptyLinePlaceholder":145},[35,438,440,443,445],{"class":37,"line":439},19,[35,441,442],{"class":80},"last ",[35,444,155],{"class":76},[35,446,447],{"class":80}," ws.max_row\n",[35,449,451,454,456,459,462,464,467,469,472,474,476,479,481,484,486,488,490,493,495,498,501,503,506,508,511,513,516],{"class":37,"line":450},20,[35,452,453],{"class":80},"table ",[35,455,155],{"class":76},[35,457,458],{"class":80}," Table(",[35,460,461],{"class":280},"displayName",[35,463,155],{"class":76},[35,465,466],{"class":45},"\"Orders\"",[35,468,192],{"class":80},[35,470,471],{"class":280},"ref",[35,473,155],{"class":76},[35,475,295],{"class":76},[35,477,478],{"class":45},"\"A1:D",[35,480,302],{"class":301},[35,482,483],{"class":80},"last",[35,485,314],{"class":301},[35,487,317],{"class":45},[35,489,192],{"class":80},[35,491,492],{"class":280},"tableStyleInfo",[35,494,155],{"class":76},[35,496,497],{"class":80},"TableStyleInfo(",[35,499,500],{"class":280},"name",[35,502,155],{"class":76},[35,504,505],{"class":45},"\"TableStyleLight9\"",[35,507,192],{"class":80},[35,509,510],{"class":280},"showRowStripes",[35,512,155],{"class":76},[35,514,515],{"class":222},"True",[35,517,518],{"class":80},"))\n",[35,520,522],{"class":37,"line":521},21,[35,523,524],{"class":80},"ws.add_table(table)\n",[35,526,528,531,534,537,540,542,545,548,551,554,556,559,562,564,567,569,572,574,577,579,582,585,587,590],{"class":37,"line":527},22,[35,529,530],{"class":222},"LATE",[35,532,533],{"class":76}," =",[35,535,536],{"class":80}," FormulaRule(",[35,538,539],{"class":280},"formula",[35,541,155],{"class":76},[35,543,544],{"class":80},"[",[35,546,547],{"class":45},"\"$C2>$B2\"",[35,549,550],{"class":80},"], ",[35,552,553],{"class":280},"fill",[35,555,155],{"class":76},[35,557,558],{"class":80},"PatternFill(",[35,560,561],{"class":45},"\"solid\"",[35,563,192],{"class":80},[35,565,566],{"class":280},"fgColor",[35,568,155],{"class":76},[35,570,571],{"class":45},"\"FFC7CE\"",[35,573,349],{"class":80},[35,575,576],{"class":280},"font",[35,578,155],{"class":76},[35,580,581],{"class":80},"Font(",[35,583,584],{"class":280},"color",[35,586,155],{"class":76},[35,588,589],{"class":45},"\"9C0006\"",[35,591,518],{"class":80},[35,593,595,598,600,602,604,606,608,611,613,615,617,619,621,623,625,627,630,632,634,636,638,641,643,645],{"class":37,"line":594},23,[35,596,597],{"class":222},"BIG",[35,599,533],{"class":76},[35,601,536],{"class":80},[35,603,539],{"class":280},[35,605,155],{"class":76},[35,607,544],{"class":80},[35,609,610],{"class":45},"\"$D2>5000\"",[35,612,550],{"class":80},[35,614,553],{"class":280},[35,616,155],{"class":76},[35,618,558],{"class":80},[35,620,561],{"class":45},[35,622,192],{"class":80},[35,624,566],{"class":280},[35,626,155],{"class":76},[35,628,629],{"class":45},"\"FFEB9C\"",[35,631,349],{"class":80},[35,633,576],{"class":280},[35,635,155],{"class":76},[35,637,581],{"class":80},[35,639,640],{"class":280},"bold",[35,642,155],{"class":76},[35,644,515],{"class":222},[35,646,518],{"class":80},[35,648,650,653,655,658,660,662,664,666,668,670],{"class":37,"line":649},24,[35,651,652],{"class":80},"ws.conditional_formatting.add(",[35,654,295],{"class":76},[35,656,657],{"class":45},"\"A2:D",[35,659,302],{"class":301},[35,661,483],{"class":80},[35,663,314],{"class":301},[35,665,317],{"class":45},[35,667,192],{"class":80},[35,669,530],{"class":222},[35,671,236],{"class":80},[35,673,675,677,679,682,684,686,688,690,692,694],{"class":37,"line":674},25,[35,676,652],{"class":80},[35,678,295],{"class":76},[35,680,681],{"class":45},"\"D2:D",[35,683,302],{"class":301},[35,685,483],{"class":80},[35,687,314],{"class":301},[35,689,317],{"class":45},[35,691,192],{"class":80},[35,693,597],{"class":222},[35,695,236],{"class":80},[35,697,699,702,705],{"class":37,"line":698},26,[35,700,701],{"class":80},"wb.save(",[35,703,704],{"class":45},"\"orders_tracker.xlsx\"",[35,706,236],{"class":80},[35,708,710,713,715,718,721,724,727,730,733,735,738,740],{"class":37,"line":709},27,[35,711,712],{"class":222},"print",[35,714,254],{"class":80},[35,716,717],{"class":45},"\"table\"",[35,719,720],{"class":80},", table.ref, ",[35,722,723],{"class":45},"\"| rule ranges:\"",[35,725,726],{"class":80},", [",[35,728,729],{"class":222},"str",[35,731,732],{"class":80},"(cf.sqref) ",[35,734,242],{"class":76},[35,736,737],{"class":80}," cf ",[35,739,248],{"class":76},[35,741,742],{"class":80}," ws.conditional_formatting])\n",[10,744,745,746,749,750,753,754,757],{},"The table covers ",[32,747,748],{},"A1:D9","; the late rule covers the data rows ",[32,751,752],{},"A2:D9"," and the amount rule ",[32,755,756],{},"D2:D9",". Both formulas are written for row 2 with the column anchored and the row relative, so each row tests its own dates and amount. Everything is consistent — for now.",[20,759,761],{"id":760},"step-2-append-a-week-of-orders-and-watch-the-rules-fall-behind","Step 2 — Append a week of orders and watch the rules fall behind",[10,763,764],{},"A later run opens the workbook, appends new rows below the table and resizes the table to include them:",[25,766,768],{"className":67,"code":767,"language":69,"meta":30,"style":30},"from openpyxl import load_workbook\n\ndef append_orders(path, new_rows):\n    wb = load_workbook(path)\n    ws = wb[\"Orders\"]\n    t = ws.tables[\"Orders\"]\n    for r in new_rows:\n        ws.append(r)\n        for c in ws[ws.max_row][1:3]:\n            c.number_format = \"yyyy-mm-dd\"\n    t.ref = f\"A1:D{ws.max_row}\"\n    wb.save(path)\n    return wb\n\nnew = [[f\"SO-{1009 + i}\", base + timedelta(days=8 + i), base + timedelta(days=9 + i), 3000 + i * 1500] for i in range(5)]\nwb = append_orders(\"orders_tracker.xlsx\", new)\nws = wb[\"Orders\"]\nprint(\"table now\", ws.tables[\"Orders\"].ref, \"| rule ranges still:\", [str(cf.sqref) for cf in ws.conditional_formatting])\n",[32,769,770,781,785,797,807,822,836,848,853,875,884,906,911,919,923,1014,1028,1040],{"__ignoreMap":30},[35,771,772,774,776,778],{"class":37,"line":38},[35,773,77],{"class":76},[35,775,95],{"class":80},[35,777,84],{"class":76},[35,779,780],{"class":80}," load_workbook\n",[35,782,783],{"class":37,"line":90},[35,784,146],{"emptyLinePlaceholder":145},[35,786,787,790,794],{"class":37,"line":103},[35,788,789],{"class":76},"def",[35,791,793],{"class":792},"s_Opv"," append_orders",[35,795,796],{"class":80},"(path, new_rows):\n",[35,798,799,802,804],{"class":37,"line":116},[35,800,801],{"class":80},"    wb ",[35,803,155],{"class":76},[35,805,806],{"class":80}," load_workbook(path)\n",[35,808,809,812,814,817,819],{"class":37,"line":129},[35,810,811],{"class":80},"    ws ",[35,813,155],{"class":76},[35,815,816],{"class":80}," wb[",[35,818,466],{"class":45},[35,820,821],{"class":80},"]\n",[35,823,824,827,829,832,834],{"class":37,"line":142},[35,825,826],{"class":80},"    t ",[35,828,155],{"class":76},[35,830,831],{"class":80}," ws.tables[",[35,833,466],{"class":45},[35,835,821],{"class":80},[35,837,838,840,843,845],{"class":37,"line":149},[35,839,412],{"class":76},[35,841,842],{"class":80}," r ",[35,844,248],{"class":76},[35,846,847],{"class":80}," new_rows:\n",[35,849,850],{"class":37,"line":161},[35,851,852],{"class":80},"        ws.append(r)\n",[35,854,855,858,860,862,865,867,870,872],{"class":37,"line":172},[35,856,857],{"class":76},"        for",[35,859,415],{"class":80},[35,861,248],{"class":76},[35,863,864],{"class":80}," ws[ws.max_row][",[35,866,233],{"class":222},[35,868,869],{"class":80},":",[35,871,404],{"class":222},[35,873,874],{"class":80},"]:\n",[35,876,877,880,882],{"class":37,"line":183},[35,878,879],{"class":80},"            c.number_format ",[35,881,155],{"class":76},[35,883,431],{"class":45},[35,885,886,889,891,894,896,898,901,903],{"class":37,"line":211},[35,887,888],{"class":80},"    t.ref ",[35,890,155],{"class":76},[35,892,893],{"class":76}," f",[35,895,478],{"class":45},[35,897,302],{"class":301},[35,899,900],{"class":80},"ws.max_row",[35,902,314],{"class":301},[35,904,905],{"class":45},"\"\n",[35,907,908],{"class":37,"line":239},[35,909,910],{"class":80},"    wb.save(path)\n",[35,912,913,916],{"class":37,"line":263},[35,914,915],{"class":76},"    return",[35,917,918],{"class":80}," wb\n",[35,920,921],{"class":37,"line":289},[35,922,146],{"emptyLinePlaceholder":145},[35,924,925,928,930,933,935,937,939,942,944,946,948,950,953,955,957,959,961,963,965,968,970,972,974,976,978,980,983,986,988,990,992,995,998,1000,1002,1004,1006,1008,1011],{"class":37,"line":367},[35,926,927],{"class":80},"new ",[35,929,155],{"class":76},[35,931,932],{"class":80}," [[",[35,934,295],{"class":76},[35,936,298],{"class":45},[35,938,302],{"class":301},[35,940,941],{"class":222},"1009",[35,943,308],{"class":76},[35,945,311],{"class":80},[35,947,314],{"class":301},[35,949,317],{"class":45},[35,951,952],{"class":80},", base ",[35,954,274],{"class":76},[35,956,277],{"class":80},[35,958,281],{"class":280},[35,960,155],{"class":76},[35,962,257],{"class":222},[35,964,308],{"class":76},[35,966,967],{"class":80}," i), base ",[35,969,274],{"class":76},[35,971,277],{"class":80},[35,973,281],{"class":280},[35,975,155],{"class":76},[35,977,228],{"class":222},[35,979,308],{"class":76},[35,981,982],{"class":80}," i), ",[35,984,985],{"class":222},"3000",[35,987,308],{"class":76},[35,989,245],{"class":80},[35,991,359],{"class":76},[35,993,994],{"class":222}," 1500",[35,996,997],{"class":80},"] ",[35,999,242],{"class":76},[35,1001,245],{"class":80},[35,1003,248],{"class":76},[35,1005,251],{"class":222},[35,1007,254],{"class":80},[35,1009,1010],{"class":222},"5",[35,1012,1013],{"class":80},")]\n",[35,1015,1016,1018,1020,1023,1025],{"class":37,"line":409},[35,1017,152],{"class":80},[35,1019,155],{"class":76},[35,1021,1022],{"class":80}," append_orders(",[35,1024,704],{"class":45},[35,1026,1027],{"class":80},", new)\n",[35,1029,1030,1032,1034,1036,1038],{"class":37,"line":423},[35,1031,164],{"class":80},[35,1033,155],{"class":76},[35,1035,816],{"class":80},[35,1037,466],{"class":45},[35,1039,821],{"class":80},[35,1041,1042,1044,1046,1049,1052,1054,1057,1060,1062,1064,1066,1068,1070,1072],{"class":37,"line":434},[35,1043,712],{"class":222},[35,1045,254],{"class":80},[35,1047,1048],{"class":45},"\"table now\"",[35,1050,1051],{"class":80},", ws.tables[",[35,1053,466],{"class":45},[35,1055,1056],{"class":80},"].ref, ",[35,1058,1059],{"class":45},"\"| rule ranges still:\"",[35,1061,726],{"class":80},[35,1063,729],{"class":222},[35,1065,732],{"class":80},[35,1067,242],{"class":76},[35,1069,737],{"class":80},[35,1071,248],{"class":76},[35,1073,742],{"class":80},[10,1075,1076,1077,1080],{},"The table now covers ",[32,1078,1079],{},"A1:D14",", but both rules still end at row 9. The five new orders — every one of them late, and three of them over 5,000 — get no highlighting at all. Nothing warns about this: the workbook opens normally, the table looks complete, and the only clue is that the newest rows are oddly plain. Rules are attached to cell ranges, not to tables, and openpyxl writes exactly the ranges it was given.",[1082,1083,1091,1092,1091,1096,1091,1100,1091,1108,1091,1117,1091,1121,1091,1128,1091,1132,1091,1136,1091,1140,1091,1144,1091,1147,1091,1151,1091,1155,1091,1159,1091,1164,1091,1168,1091,1170,1091,1173,1091,1175,1091,1178,1091,1180,1091,1183,1091,1186,1091,1189,1091,1193,1091,1197,1091,1199,1091,1202,1091,1204,1091,1207,1091,1209,1091,1212,1091,1215,1091,1218,1091,1220,1091,1224,1091,1226,1091,1229,1091,1233,1091,1237,1091,1239],"svg",{"viewBox":1084,"role":1085,"ariaLabelledBy":1086,"xmlns":1089,"style":1090},"180 0 399 140","img",[1087,1088],"grow-gap-t","grow-gap-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:399px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[1093,1094,1095],"title",{"id":1087},"The table grew, the rules did not",[1097,1098,1099],"desc",{"id":1088},"The table now runs to row 14, but both conditional formatting ranges still end at row 9. The five rows added in the second run, all late and three over 5,000, are not highlighted.",[1101,1102],"rect",{"x":1103,"y":1104,"width":1105,"height":1106,"fill":1107},"180","0","399","140","#ffffff",[1101,1109],{"x":1110,"y":1111,"width":1112,"height":1113,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"196.8","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[1101,1118],{"x":1119,"y":1111,"width":1120,"height":1113,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"228.8","53.9",[1122,1123,1127],"text",{"x":1124,"y":1125,"style":1126},"255.7","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[1101,1129],{"x":1130,"y":1111,"width":1131,"height":1113,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"282.7","84.7",[1122,1133,1135],{"x":1134,"y":1125,"style":1126},"325.0","B",[1101,1137],{"x":1138,"y":1111,"width":1139,"height":1113,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"367.4","97.9",[1122,1141,1143],{"x":1142,"y":1125,"style":1126},"416.3","C",[1101,1145],{"x":1146,"y":1111,"width":1139,"height":1113,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"465.3",[1122,1148,1150],{"x":1149,"y":1125,"style":1126},"514.3","D",[1101,1152],{"x":1110,"y":1153,"width":1112,"height":1154,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"40.0","28.0",[1122,1156,233],{"x":1157,"y":1158,"style":1126},"212.8","59.0",[1101,1160],{"x":1119,"y":1153,"width":1120,"height":1154,"rx":1104,"fill":1161,"stroke":1162,"style":1163},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[1122,1165,1167],{"x":1124,"y":1158,"style":1166},"font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Rows",[1101,1169],{"x":1130,"y":1153,"width":1131,"height":1154,"rx":1104,"fill":1161,"stroke":1162,"style":1163},[1122,1171,1172],{"x":1134,"y":1158,"style":1166},"Table",[1101,1174],{"x":1138,"y":1153,"width":1139,"height":1154,"rx":1104,"fill":1161,"stroke":1162,"style":1163},[1122,1176,1177],{"x":1142,"y":1158,"style":1166},"Late rule",[1101,1179],{"x":1146,"y":1153,"width":1139,"height":1154,"rx":1104,"fill":1161,"stroke":1162,"style":1163},[1122,1181,1182],{"x":1149,"y":1158,"style":1166},"Big rule",[1101,1184],{"x":1110,"y":1185,"width":1112,"height":1154,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"68.0",[1122,1187,385],{"x":1157,"y":1188,"style":1126},"87.0",[1101,1190],{"x":1119,"y":1185,"width":1120,"height":1154,"rx":1104,"fill":1191,"stroke":1192,"style":1163},"#d9f4f1","var(--teal,#0f9488)",[1122,1194,1196],{"x":1124,"y":1188,"style":1195},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","2–9",[1101,1198],{"x":1130,"y":1185,"width":1131,"height":1154,"rx":1104,"fill":1191,"stroke":1192,"style":1163},[1122,1200,1201],{"x":1134,"y":1188,"style":1195},"✓",[1101,1203],{"x":1138,"y":1185,"width":1139,"height":1154,"rx":1104,"fill":1191,"stroke":1192,"style":1163},[1122,1205,1206],{"x":1142,"y":1188,"style":1195},"A2:D9 ✓",[1101,1208],{"x":1146,"y":1185,"width":1139,"height":1154,"rx":1104,"fill":1191,"stroke":1192,"style":1163},[1122,1210,1211],{"x":1149,"y":1188,"style":1195},"D2:D9 ✓",[1101,1213],{"x":1110,"y":1214,"width":1112,"height":1154,"rx":1104,"fill":1114,"stroke":1115,"style":1116},"96.0",[1122,1216,404],{"x":1157,"y":1217,"style":1126},"115.0",[1101,1219],{"x":1119,"y":1214,"width":1120,"height":1154,"rx":1104,"fill":1114,"stroke":1115,"style":1163},[1122,1221,1223],{"x":1124,"y":1217,"style":1222},"font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","10–14",[1101,1225],{"x":1130,"y":1214,"width":1131,"height":1154,"rx":1104,"fill":1191,"stroke":1192,"style":1163},[1122,1227,1228],{"x":1134,"y":1217,"style":1195},"✓ (resized)",[1101,1230],{"x":1138,"y":1214,"width":1139,"height":1154,"rx":1104,"fill":1231,"stroke":1232,"style":1163},"#fce9e9","var(--danger,#dc2626)",[1122,1234,1236],{"x":1142,"y":1217,"style":1235},"font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","✗ not covered",[1101,1238],{"x":1146,"y":1214,"width":1139,"height":1154,"rx":1104,"fill":1231,"stroke":1232,"style":1163},[1122,1240,1236],{"x":1149,"y":1217,"style":1235},[20,1242,1244],{"id":1243},"step-3-re-point-every-rule-at-the-tables-current-range","Step 3 — Re-point every rule at the table's current range",[10,1246,1247],{},"The robust fix is to rebuild the conditional formatting after every append, keeping each rule and its column span but extending its rows to the table's last row:",[25,1249,1251],{"className":67,"code":1250,"language":69,"meta":30,"style":30},"from openpyxl.formatting.formatting import ConditionalFormattingList\nfrom openpyxl.utils.cell import range_boundaries, get_column_letter\n\ndef stretch_rules_to_table(ws, table_name):\n    t = ws.tables[table_name]\n    _, top, _, bottom = range_boundaries(t.ref)\n    old = ws.conditional_formatting\n    ws.conditional_formatting = ConditionalFormattingList()\n    for cf in old:\n        for rng in str(cf.sqref).split():\n            c1, r1, c2, _ = range_boundaries(rng)\n            new_ref = f\"{get_column_letter(c1)}{r1}:{get_column_letter(c2)}{bottom}\"\n            for rule in cf.rules:\n                ws.conditional_formatting.add(new_ref, rule)\n\nstretch_rules_to_table(ws, \"Orders\")\nwb.save(\"orders_tracker.xlsx\")\nprint(\"rule ranges now:\", [str(cf.sqref) for cf in ws.conditional_formatting])\n",[32,1252,1253,1265,1277,1281,1291,1300,1310,1320,1330,1341,1356,1366,1406,1419,1424,1428,1437,1445],{"__ignoreMap":30},[35,1254,1255,1257,1260,1262],{"class":37,"line":38},[35,1256,77],{"class":76},[35,1258,1259],{"class":80}," openpyxl.formatting.formatting ",[35,1261,84],{"class":76},[35,1263,1264],{"class":80}," ConditionalFormattingList\n",[35,1266,1267,1269,1272,1274],{"class":37,"line":90},[35,1268,77],{"class":76},[35,1270,1271],{"class":80}," openpyxl.utils.cell ",[35,1273,84],{"class":76},[35,1275,1276],{"class":80}," range_boundaries, get_column_letter\n",[35,1278,1279],{"class":37,"line":103},[35,1280,146],{"emptyLinePlaceholder":145},[35,1282,1283,1285,1288],{"class":37,"line":116},[35,1284,789],{"class":76},[35,1286,1287],{"class":792}," stretch_rules_to_table",[35,1289,1290],{"class":80},"(ws, table_name):\n",[35,1292,1293,1295,1297],{"class":37,"line":129},[35,1294,826],{"class":80},[35,1296,155],{"class":76},[35,1298,1299],{"class":80}," ws.tables[table_name]\n",[35,1301,1302,1305,1307],{"class":37,"line":142},[35,1303,1304],{"class":80},"    _, top, _, bottom ",[35,1306,155],{"class":76},[35,1308,1309],{"class":80}," range_boundaries(t.ref)\n",[35,1311,1312,1315,1317],{"class":37,"line":149},[35,1313,1314],{"class":80},"    old ",[35,1316,155],{"class":76},[35,1318,1319],{"class":80}," ws.conditional_formatting\n",[35,1321,1322,1325,1327],{"class":37,"line":161},[35,1323,1324],{"class":80},"    ws.conditional_formatting ",[35,1326,155],{"class":76},[35,1328,1329],{"class":80}," ConditionalFormattingList()\n",[35,1331,1332,1334,1336,1338],{"class":37,"line":172},[35,1333,412],{"class":76},[35,1335,737],{"class":80},[35,1337,248],{"class":76},[35,1339,1340],{"class":80}," old:\n",[35,1342,1343,1345,1348,1350,1353],{"class":37,"line":183},[35,1344,857],{"class":76},[35,1346,1347],{"class":80}," rng ",[35,1349,248],{"class":76},[35,1351,1352],{"class":222}," str",[35,1354,1355],{"class":80},"(cf.sqref).split():\n",[35,1357,1358,1361,1363],{"class":37,"line":211},[35,1359,1360],{"class":80},"            c1, r1, c2, _ ",[35,1362,155],{"class":76},[35,1364,1365],{"class":80}," range_boundaries(rng)\n",[35,1367,1368,1371,1373,1375,1377,1379,1382,1385,1388,1390,1392,1394,1397,1399,1402,1404],{"class":37,"line":239},[35,1369,1370],{"class":80},"            new_ref ",[35,1372,155],{"class":76},[35,1374,893],{"class":76},[35,1376,317],{"class":45},[35,1378,302],{"class":301},[35,1380,1381],{"class":80},"get_column_letter(c1)",[35,1383,1384],{"class":301},"}{",[35,1386,1387],{"class":80},"r1",[35,1389,314],{"class":301},[35,1391,869],{"class":45},[35,1393,302],{"class":301},[35,1395,1396],{"class":80},"get_column_letter(c2)",[35,1398,1384],{"class":301},[35,1400,1401],{"class":80},"bottom",[35,1403,314],{"class":301},[35,1405,905],{"class":45},[35,1407,1408,1411,1414,1416],{"class":37,"line":263},[35,1409,1410],{"class":76},"            for",[35,1412,1413],{"class":80}," rule ",[35,1415,248],{"class":76},[35,1417,1418],{"class":80}," cf.rules:\n",[35,1420,1421],{"class":37,"line":289},[35,1422,1423],{"class":80},"                ws.conditional_formatting.add(new_ref, rule)\n",[35,1425,1426],{"class":37,"line":367},[35,1427,146],{"emptyLinePlaceholder":145},[35,1429,1430,1433,1435],{"class":37,"line":409},[35,1431,1432],{"class":80},"stretch_rules_to_table(ws, ",[35,1434,466],{"class":45},[35,1436,236],{"class":80},[35,1438,1439,1441,1443],{"class":37,"line":423},[35,1440,701],{"class":80},[35,1442,704],{"class":45},[35,1444,236],{"class":80},[35,1446,1447,1449,1451,1454,1456,1458,1460,1462,1464,1466],{"class":37,"line":434},[35,1448,712],{"class":222},[35,1450,254],{"class":80},[35,1452,1453],{"class":45},"\"rule ranges now:\"",[35,1455,726],{"class":80},[35,1457,729],{"class":222},[35,1459,732],{"class":80},[35,1461,242],{"class":76},[35,1463,737],{"class":80},[35,1465,248],{"class":76},[35,1467,742],{"class":80},[10,1469,1470,1471,1474],{},"Each rule keeps its starting row and columns — the late rule still starts at ",[32,1472,1473],{},"A2"," and spans A to D, the amount rule stays in column D — and its last row becomes the table's last row. Because the formulas are written relative to the first cell, extending the range is all it takes for the new rows to be tested correctly; the formula text does not change. Rebuilding the list rather than editing ranges in place avoids duplicate entries, and the rules keep their original order and priority. Call this function at the end of every job that changes the table's size — it is idempotent, so calling it when nothing changed is harmless.",[1082,1476,1091,1482,1091,1485,1091,1488,1091,1493,1091,1500,1091,1511,1091,1517,1091,1522,1091,1527,1091,1532,1091,1535,1091,1542,1091,1547,1091,1550,1091,1554,1091,1557,1091,1560,1091,1567,1091,1573,1091,1577,1091,1581,1091,1585,1091,1588,1091,1593,1091,1600,1091,1605],{"viewBox":1477,"role":1085,"ariaLabelledBy":1478,"xmlns":1089,"style":1481},"4 0 752 142",[1479,1480],"grow-fix-t","grow-fix-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[1093,1483,1484],{"id":1479},"Every append ends by stretching the rules",[1097,1486,1487],{"id":1480},"The job appends the new rows, resizes the table reference to the new last row, rebuilds every conditional formatting rule with its range extended to that row, and checks that every rule now covers every data row.",[1101,1489],{"x":1490,"y":1104,"width":1491,"height":1492,"fill":1107},"4","752","142",[1101,1494],{"x":1495,"y":1111,"width":1496,"height":1497,"rx":1498,"fill":1114,"stroke":1115,"style":1499},"20.0","153.0","110.5","12","stroke-width:2px",[1501,1502,1504],"g",{"transform":1503},"translate(84.5,37.8) scale(1.000)",[1505,1506],"path",{"d":1507,"fill":1508,"stroke":1509,"style":1510},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[1122,1512,1516],{"x":1513,"y":1514,"style":1515},"96.5","84.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Append rows",[1122,1518,1521],{"x":1513,"y":1519,"style":1520},"103.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","ws.append(...)",[37,1523],{"x1":1524,"y1":1525,"x2":1526,"y2":1525,"stroke":1162,"style":1499},"178.0","71.2","200.0",[1528,1529],"polygon",{"points":1530,"fill":1531},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[1101,1533],{"x":1534,"y":1111,"width":1496,"height":1497,"rx":1498,"fill":1161,"stroke":1162,"style":1499},"209.0",[1501,1536,1538],{"transform":1537},"translate(273.5,37.8) scale(1.000)",[1505,1539],{"d":1540,"fill":1508,"stroke":1541,"style":1510},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--brand-strong,#4338ca)",[1122,1543,1546],{"x":1544,"y":1514,"style":1545},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Resize table",[1122,1548,1549],{"x":1544,"y":1519,"style":1520},"t.ref = A1:D{last}",[37,1551],{"x1":1552,"y1":1525,"x2":1553,"y2":1525,"stroke":1162,"style":1499},"367.0","389.0",[1528,1555],{"points":1556,"fill":1531},"393.0,71.2 384.0,76.2 384.0,66.2",[1101,1558],{"x":1559,"y":1111,"width":1496,"height":1497,"rx":1498,"fill":1191,"stroke":1192,"style":1499},"398.0",[1501,1561,1563],{"transform":1562},"translate(462.5,29.0) scale(1.000)",[1505,1564],{"d":1565,"fill":1508,"stroke":1566,"style":1510},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--teal-ink,#0b6157)",[1122,1568,1572],{"x":1569,"y":1570,"style":1571},"474.5","75.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Stretch rules",[1122,1574,1576],{"x":1569,"y":1575,"style":1520},"95.0","same columns, new",[1122,1578,1580],{"x":1569,"y":1579,"style":1520},"112.5","last row",[37,1582],{"x1":1583,"y1":1525,"x2":1584,"y2":1525,"stroke":1162,"style":1499},"556.0","578.0",[1528,1586],{"points":1587,"fill":1531},"582.0,71.2 573.0,76.2 573.0,66.2",[1101,1589],{"x":1590,"y":1111,"width":1496,"height":1497,"rx":1498,"fill":1591,"stroke":1592,"style":1499},"587.0","#fdefd8","var(--gold,#b4740a)",[1501,1594,1596],{"transform":1595},"translate(651.5,37.8) scale(1.000)",[1505,1597],{"d":1598,"fill":1508,"stroke":1599,"style":1510},"M5 12.5l4.5 4.5L19 7","var(--gold-ink,#7a4e06)",[1122,1601,1604],{"x":1602,"y":1514,"style":1603},"663.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Check coverage",[1122,1606,1607],{"x":1602,"y":1519,"style":1520},"no row left plain",[20,1609,1611],{"id":1610},"step-4-oversized-ranges-simpler-with-a-cost","Step 4 — Oversized ranges: simpler, with a cost",[10,1613,1614,1615,1618],{},"A common shortcut is to apply rules far below the data from the start — ",[32,1616,1617],{},"A2:D10000"," — so new rows fall inside them. It works, but it has costs worth knowing:",[25,1620,1622],{"className":67,"code":1621,"language":69,"meta":30,"style":30},"from openpyxl import Workbook\n\nalt = Workbook()\naws = alt.active\naws.append([\"Order\", \"Due\", \"Delivered\", \"Amount\"])\naws.conditional_formatting.add(\"A2:D10000\", FormulaRule(formula=[\"AND($C2\u003C>\\\"\\\",$C2>$B2)\"], fill=PatternFill(\"solid\", fgColor=\"FFC7CE\")))\nalt.save(\"oversized.xlsx\")\nprint(\"rule range:\", [str(cf.sqref) for cf in aws.conditional_formatting], \"| sheet dimension:\", aws.dimensions)\n",[32,1623,1624,1634,1638,1647,1657,1678,1725,1735],{"__ignoreMap":30},[35,1625,1626,1628,1630,1632],{"class":37,"line":38},[35,1627,77],{"class":76},[35,1629,95],{"class":80},[35,1631,84],{"class":76},[35,1633,100],{"class":80},[35,1635,1636],{"class":37,"line":90},[35,1637,146],{"emptyLinePlaceholder":145},[35,1639,1640,1643,1645],{"class":37,"line":103},[35,1641,1642],{"class":80},"alt ",[35,1644,155],{"class":76},[35,1646,158],{"class":80},[35,1648,1649,1652,1654],{"class":37,"line":116},[35,1650,1651],{"class":80},"aws ",[35,1653,155],{"class":76},[35,1655,1656],{"class":80}," alt.active\n",[35,1658,1659,1662,1664,1666,1668,1670,1672,1674,1676],{"class":37,"line":129},[35,1660,1661],{"class":80},"aws.append([",[35,1663,189],{"class":45},[35,1665,192],{"class":80},[35,1667,195],{"class":45},[35,1669,192],{"class":80},[35,1671,200],{"class":45},[35,1673,192],{"class":80},[35,1675,205],{"class":45},[35,1677,208],{"class":80},[35,1679,1680,1683,1686,1689,1691,1693,1695,1698,1701,1704,1706,1708,1710,1712,1714,1716,1718,1720,1722],{"class":37,"line":142},[35,1681,1682],{"class":80},"aws.conditional_formatting.add(",[35,1684,1685],{"class":45},"\"A2:D10000\"",[35,1687,1688],{"class":80},", FormulaRule(",[35,1690,539],{"class":280},[35,1692,155],{"class":76},[35,1694,544],{"class":80},[35,1696,1697],{"class":45},"\"AND($C2\u003C>",[35,1699,1700],{"class":301},"\\\"\\\"",[35,1702,1703],{"class":45},",$C2>$B2)\"",[35,1705,550],{"class":80},[35,1707,553],{"class":280},[35,1709,155],{"class":76},[35,1711,558],{"class":80},[35,1713,561],{"class":45},[35,1715,192],{"class":80},[35,1717,566],{"class":280},[35,1719,155],{"class":76},[35,1721,571],{"class":45},[35,1723,1724],{"class":80},")))\n",[35,1726,1727,1730,1733],{"class":37,"line":149},[35,1728,1729],{"class":80},"alt.save(",[35,1731,1732],{"class":45},"\"oversized.xlsx\"",[35,1734,236],{"class":80},[35,1736,1737,1739,1741,1744,1746,1748,1750,1752,1754,1756,1759,1762],{"class":37,"line":161},[35,1738,712],{"class":222},[35,1740,254],{"class":80},[35,1742,1743],{"class":45},"\"rule range:\"",[35,1745,726],{"class":80},[35,1747,729],{"class":222},[35,1749,732],{"class":80},[35,1751,242],{"class":76},[35,1753,737],{"class":80},[35,1755,248],{"class":76},[35,1757,1758],{"class":80}," aws.conditional_formatting], ",[35,1760,1761],{"class":45},"\"| sheet dimension:\"",[35,1763,1764],{"class":80},", aws.dimensions)\n",[10,1766,1767,1768,1772,1773,1776],{},"The rule covers 40,000 cells while the sheet holds only a header. Every rule over a huge range is evaluated across that whole range whenever Excel redraws — a few such rules are fine, dozens of them slow workbooks noticeably (see ",[14,1769,1771],{"href":1770},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Ffind-out-why-an-excel-file-is-slow-to-open\u002F","Find Out Why an Excel File Is Slow to Open","). The formula also has to cope with empty rows: here ",[32,1774,1775],{},"$C2\u003C>\"\""," stops blank rows being compared, because an empty delivery date would otherwise make some rules fire on empty rows. And the range still runs out eventually. Oversized ranges suit a sheet a person edits by hand; for a scripted job, re-pointing the rules after each append is cleaner.",[20,1778,1780],{"id":1779},"step-5-why-not-use-table-references-in-the-rule","Step 5 — Why not use table references in the rule?",[10,1782,1783,1784,1787,1788,1791],{},"It is tempting to write ",[32,1785,1786],{},"Orders[Delivered]>Orders[Due]"," so the rule follows the table automatically. Excel does not allow it: structured references cannot be used directly in conditional-formatting or data-validation formulas. The workaround people use is ",[32,1789,1790],{},"INDIRECT(\"Orders[Delivered]\")",", which works but makes the rule volatile and hard to read. Relative cell references with a range kept in step with the table, as in Step 3, are the dependable choice. Check the result after every run:",[25,1793,1795],{"className":67,"code":1794,"language":69,"meta":30,"style":30},"def uncovered_rows(ws, table_name):\n    _, top, _, bottom = range_boundaries(ws.tables[table_name].ref)\n    gaps = {}\n    for cf in ws.conditional_formatting:\n        covered = set()\n        for rng in str(cf.sqref).split():\n            _, r1, _, r2 = range_boundaries(rng)\n            covered.update(range(r1, r2 + 1))\n        missing = [r for r in range(top + 1, bottom + 1) if r not in covered]\n        if missing:\n            gaps[str(cf.sqref)] = missing\n    return gaps\n\ncheck = load_workbook(\"orders_tracker.xlsx\")[\"Orders\"]\nprint(\"rows without highlighting:\", uncovered_rows(check, \"Orders\") or \"none\")\n",[32,1796,1797,1806,1815,1825,1836,1849,1861,1870,1887,1935,1943,1958,1965,1969,1988],{"__ignoreMap":30},[35,1798,1799,1801,1804],{"class":37,"line":38},[35,1800,789],{"class":76},[35,1802,1803],{"class":792}," uncovered_rows",[35,1805,1290],{"class":80},[35,1807,1808,1810,1812],{"class":37,"line":90},[35,1809,1304],{"class":80},[35,1811,155],{"class":76},[35,1813,1814],{"class":80}," range_boundaries(ws.tables[table_name].ref)\n",[35,1816,1817,1820,1822],{"class":37,"line":103},[35,1818,1819],{"class":80},"    gaps ",[35,1821,155],{"class":76},[35,1823,1824],{"class":80}," {}\n",[35,1826,1827,1829,1831,1833],{"class":37,"line":116},[35,1828,412],{"class":76},[35,1830,737],{"class":80},[35,1832,248],{"class":76},[35,1834,1835],{"class":80}," ws.conditional_formatting:\n",[35,1837,1838,1841,1843,1846],{"class":37,"line":129},[35,1839,1840],{"class":80},"        covered ",[35,1842,155],{"class":76},[35,1844,1845],{"class":222}," set",[35,1847,1848],{"class":80},"()\n",[35,1850,1851,1853,1855,1857,1859],{"class":37,"line":142},[35,1852,857],{"class":76},[35,1854,1347],{"class":80},[35,1856,248],{"class":76},[35,1858,1352],{"class":222},[35,1860,1355],{"class":80},[35,1862,1863,1866,1868],{"class":37,"line":149},[35,1864,1865],{"class":80},"            _, r1, _, r2 ",[35,1867,155],{"class":76},[35,1869,1365],{"class":80},[35,1871,1872,1875,1878,1881,1883,1885],{"class":37,"line":161},[35,1873,1874],{"class":80},"            covered.update(",[35,1876,1877],{"class":222},"range",[35,1879,1880],{"class":80},"(r1, r2 ",[35,1882,274],{"class":76},[35,1884,346],{"class":222},[35,1886,518],{"class":80},[35,1888,1889,1892,1894,1897,1899,1901,1903,1905,1908,1910,1912,1915,1917,1919,1921,1924,1926,1929,1932],{"class":37,"line":172},[35,1890,1891],{"class":80},"        missing ",[35,1893,155],{"class":76},[35,1895,1896],{"class":80}," [r ",[35,1898,242],{"class":76},[35,1900,842],{"class":80},[35,1902,248],{"class":76},[35,1904,251],{"class":222},[35,1906,1907],{"class":80},"(top ",[35,1909,274],{"class":76},[35,1911,346],{"class":222},[35,1913,1914],{"class":80},", bottom ",[35,1916,274],{"class":76},[35,1918,346],{"class":222},[35,1920,340],{"class":80},[35,1922,1923],{"class":76},"if",[35,1925,842],{"class":80},[35,1927,1928],{"class":76},"not",[35,1930,1931],{"class":76}," in",[35,1933,1934],{"class":80}," covered]\n",[35,1936,1937,1940],{"class":37,"line":183},[35,1938,1939],{"class":76},"        if",[35,1941,1942],{"class":80}," missing:\n",[35,1944,1945,1948,1950,1953,1955],{"class":37,"line":211},[35,1946,1947],{"class":80},"            gaps[",[35,1949,729],{"class":222},[35,1951,1952],{"class":80},"(cf.sqref)] ",[35,1954,155],{"class":76},[35,1956,1957],{"class":80}," missing\n",[35,1959,1960,1962],{"class":37,"line":239},[35,1961,915],{"class":76},[35,1963,1964],{"class":80}," gaps\n",[35,1966,1967],{"class":37,"line":263},[35,1968,146],{"emptyLinePlaceholder":145},[35,1970,1971,1974,1976,1979,1981,1984,1986],{"class":37,"line":289},[35,1972,1973],{"class":80},"check ",[35,1975,155],{"class":76},[35,1977,1978],{"class":80}," load_workbook(",[35,1980,704],{"class":45},[35,1982,1983],{"class":80},")[",[35,1985,466],{"class":45},[35,1987,821],{"class":80},[35,1989,1990,1992,1994,1997,2000,2002,2004,2007,2010],{"class":37,"line":367},[35,1991,712],{"class":222},[35,1993,254],{"class":80},[35,1995,1996],{"class":45},"\"rows without highlighting:\"",[35,1998,1999],{"class":80},", uncovered_rows(check, ",[35,2001,466],{"class":45},[35,2003,340],{"class":80},[35,2005,2006],{"class":76},"or",[35,2008,2009],{"class":45}," \"none\"",[35,2011,236],{"class":80},[10,2013,2014,2015,18],{},"The check compares every rule's rows with the table's data rows and lists any that are not covered. After the stretch it finds none; run against the workbook from Step 2 it would have listed rows 10–14 for both rules. A test like this at the end of the job turns an invisible problem into a failed run. Rule order and priorities are unaffected by the stretch, which matters when rules overlap — see ",[14,2016,2018],{"href":2017},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fcontrol-conditional-formatting-rule-order-and-stop-if-true\u002F","Control Conditional Formatting Rule Order and Stop If True",[1082,2020,1091,2025,1091,2028,1091,2031,1091,2034,1091,2039,1091,2045,1091,2051,1091,2056,1091,2061,1091,2065,1091,2069,1091,2072,1091,2077,1091,2082,1091,2086,1091,2089,1091,2092,1091,2095,1091,2098,1091,2105,1091,2110,1091,2114,1091,2117,1091,2120],{"viewBox":2021,"role":1085,"ariaLabelledBy":2022,"xmlns":1089,"style":1481},"4 0 752 207",[2023,2024],"grow-ways-t","grow-ways-d",[1093,2026,2027],{"id":2023},"Three ways to keep rules covering a growing table",[1097,2029,2030],{"id":2024},"Re-pointing rules after each append keeps ranges exact and fast but needs code in every job. Oversized ranges need no code but cost performance and must cope with blank rows. INDIRECT with table references follows the table but is volatile and hard to read.",[1101,2032],{"x":1490,"y":1104,"width":1491,"height":2033,"fill":1107},"207",[1101,2035],{"x":1495,"y":1111,"width":2036,"height":2037,"rx":2038,"fill":1191,"stroke":1192,"style":1499},"228.0","175.0","14",[1501,2040,2042],{"transform":2041},"translate(121.0,29.0) scale(1.083)",[1505,2043],{"d":1598,"fill":1508,"stroke":1566,"style":2044},"stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[1122,2046,2050],{"x":2047,"y":2048,"style":2049},"134.0","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","re-point after append",[37,2052],{"x1":2053,"y1":2054,"x2":2055,"y2":2054,"stroke":1192,"style":1116},"36.0","91.0","232.0",[1122,2057,2060],{"x":2047,"y":2058,"style":2059},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","exact ranges",[1122,2062,2064],{"x":2047,"y":2063,"style":2059},"137.0","no blank-row logic",[1122,2066,2068],{"x":2047,"y":2067,"style":2059},"163.0","needs a step in the job",[1101,2070],{"x":2071,"y":1111,"width":2036,"height":2037,"rx":2038,"fill":1591,"stroke":1592,"style":1499},"266.0",[1501,2073,2075],{"transform":2074},"translate(367.0,29.0) scale(1.083)",[1505,2076],{"d":1540,"fill":1508,"stroke":1599,"style":2044},[1122,2078,2081],{"x":2079,"y":2048,"style":2080},"380.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","oversized ranges",[37,2083],{"x1":2084,"y1":2054,"x2":2085,"y2":2054,"stroke":1592,"style":1116},"282.0","478.0",[1122,2087,2088],{"x":2079,"y":2058,"style":2059},"no code",[1122,2090,2091],{"x":2079,"y":2063,"style":2059},"slower, blank rows to handle",[1122,2093,2094],{"x":2079,"y":2067,"style":2059},"still runs out",[1101,2096],{"x":2097,"y":1111,"width":2036,"height":2037,"rx":2038,"fill":1231,"stroke":1232,"style":1499},"512.0",[1501,2099,2101],{"transform":2100},"translate(613.0,29.0) scale(1.083)",[1505,2102],{"d":2103,"fill":1508,"stroke":2104,"style":2044},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)",[1122,2106,2109],{"x":2107,"y":2048,"style":2108},"626.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","INDIRECT('T[Col]')",[37,2111],{"x1":2112,"y1":2054,"x2":2113,"y2":2054,"stroke":1232,"style":1116},"528.0","724.0",[1122,2115,2116],{"x":2107,"y":2058,"style":2059},"follows the table",[1122,2118,2119],{"x":2107,"y":2063,"style":2059},"volatile, recalculates always",[1122,2121,2122],{"x":2107,"y":2067,"style":2059},"hard to read",[20,2124,2126],{"id":2125},"common-pitfalls","Common pitfalls",[2128,2129,2130,2138,2144,2150,2156],"ul",{},[2131,2132,2133,2137],"li",{},[2134,2135,2136],"strong",{},"Assuming rules follow the table."," Ranges written by a script stay where they were; stretch them.",[2131,2139,2140,2143],{},[2134,2141,2142],{},"Editing ranges in place."," Rebuild the list so rules are not duplicated.",[2131,2145,2146,2149],{},[2134,2147,2148],{},"Structured references in rule formulas."," Excel rejects them; use relative references.",[2131,2151,2152,2155],{},[2134,2153,2154],{},"Oversized ranges without blank-row handling."," Rules can fire on empty rows.",[2131,2157,2158,2161],{},[2134,2159,2160],{},"No coverage check."," Uncovered rows are invisible until someone notices.",[20,2163,2165],{"id":2164},"conclusion","Conclusion",[10,2167,2168],{},"Conditional formatting rules are attached to cell ranges, so when a script appends rows and resizes an Excel table, the rules stop at the old last row. After every append, rebuild the conditional formatting with each rule's range stretched to the table's new last row — the relative formulas then test the new rows correctly — and check that every data row is covered by every rule. Oversized ranges are a simpler alternative for hand-edited sheets, at a performance cost, and structured references cannot be used in rule formulas at all.",[20,2170,2172],{"id":2171},"frequently-asked-questions","Frequently asked questions",[10,2174,2175,2178],{},[2134,2176,2177],{},"Why doesn't conditional formatting apply to new rows added to my Excel table?","\nThe rules have fixed cell ranges. Rows added by a script below the old range are outside it, even if the table itself was resized. Extend each rule's range to the new last row.",[10,2180,2181,2184],{},[2134,2182,2183],{},"How do I extend conditional formatting ranges with openpyxl?","\nRead each rule's range and rules, replace the worksheet's conditional_formatting with a new ConditionalFormattingList, and add each rule again with the range's last row set to the table's last row.",[10,2186,2187,2190],{},[2134,2188,2189],{},"Can I use table column references in conditional formatting?","\nNot directly; Excel does not allow structured references in conditional formatting formulas. INDIRECT with a table reference works but is volatile.",[10,2192,2193,2196],{},[2134,2194,2195],{},"Is it OK to apply conditional formatting to thousands of empty rows?","\nIt works, but every rule is evaluated across its whole range, which slows large workbooks, and formulas must ignore blank rows.",[20,2198,2200],{"id":2199},"related","Related",[2128,2202,2203,2209,2214,2221],{},[2131,2204,2205,2206,2208],{},"Up one level: ",[14,2207,17],{"href":16}," — rule-based formatting.",[2131,2210,2211,2213],{},[14,2212,56],{"href":55}," — the append itself.",[2131,2215,2216,2220],{},[14,2217,2219],{"href":2218},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Flist-and-remove-conditional-formatting-with-openpyxl\u002F","List and Remove Conditional Formatting with openpyxl"," — inspecting rules and ranges.",[2131,2222,2223,2227],{},[14,2224,2226],{"href":2225},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data\u002F","Make an Excel Chart Grow with Its Data"," — the same problem for charts.",[2229,2230,2231],"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":30,"searchDepth":90,"depth":90,"links":2233},[2234,2235,2236,2237,2238,2239,2240,2241,2242,2243],{"id":22,"depth":90,"text":23},{"id":60,"depth":90,"text":61},{"id":760,"depth":90,"text":761},{"id":1243,"depth":90,"text":1244},{"id":1610,"depth":90,"text":1611},{"id":1779,"depth":90,"text":1780},{"id":2125,"depth":90,"text":2126},{"id":2164,"depth":90,"text":2165},{"id":2171,"depth":90,"text":2172},{"id":2199,"depth":90,"text":2200},"2026-10-02","Keep highlighting working when a report table gets new rows every week: why conditional formatting ranges do not follow rows appended by Python, re-pointing every rule at the resized table with openpyxl, generous ranges as a simpler alternative and their cost, why structured references are not allowed in rule formulas, and a check that every rule covers every data row after an append.","md",[2248,2250,2252,2254],{"q":2177,"a":2249},"The rules have fixed cell ranges. Rows added by a script below the old range are outside it, even if the table itself was resized. Extend each rule's range to the new last row.",{"q":2183,"a":2251},"Read each rule's range and rules, replace the worksheet's conditional_formatting with a new ConditionalFormattingList, and add each rule again with the range's last row set to the table's last row.",{"q":2189,"a":2253},"Not directly; Excel does not allow structured references in conditional formatting formulas. INDIRECT with a table reference works but is volatile.",{"q":2195,"a":2255},"It works, but every rule is evaluated across its whole range, which slows large workbooks, and formulas must ignore blank rows.","apply conditional formatting to an excel table that grows",null,{"breadcrumb":2259},[2260,2263,2266],{"name":2261,"item":2262},"Home","\u002F",{"name":2264,"item":2265},"Advanced Data Transformation and Cleaning for Excel Automation","\u002Fadvanced-data-transformation-and-cleaning\u002F",{"name":2267,"item":16},"Applying Conditional Formatting With openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows",{"title":2270,"description":2271},"Conditional Formatting on a Growing Excel Table with openpyxl","Make openpyxl conditional formatting follow a growing Excel table: resize the table, rebuild rule ranges to match, compare with oversized ranges, avoid structured references in rule formulas, and verify coverage after each append.","apply-conditional-formatting-to-an-excel-table-that-grows","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows\u002Findex",[2275,2276],"conditional-formatting","tables-and-filters","how-to","y54mP5WfxQjLc1SZRArVKwoUHJq5h8Y5avYxQuctMhU",[2280,2284],{"title":2281,"path":2282,"stem":2283,"children":-1},"Apply a Colour Scale per Row or per Column with openpyxl","\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-a-colour-scale-per-row-or-per-column-with-openpyxl","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-a-colour-scale-per-row-or-per-column-with-openpyxl\u002Findex",{"title":2018,"path":2285,"stem":2286,"children":-1},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fcontrol-conditional-formatting-rule-order-and-stop-if-true","advanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fcontrol-conditional-formatting-rule-order-and-stop-if-true\u002Findex",[2288,2290],{"path":2289,"title":2267},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl",{"path":2291,"title":2264},"\u002Fadvanced-data-transformation-and-cleaning",[],[2294,2297,2300,2303,2304,2305],{"path":2295,"title":2296,"kind":2277},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fapply-conditional-formatting-with-xlsxwriter","Apply Conditional Formatting with xlsxwriter",{"path":2298,"title":2299,"kind":2277},"\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":2301,"title":2302,"kind":2277},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fadd-icon-sets-with-openpyxl","Add Icon Sets to Excel Cells with openpyxl",{"path":2282,"title":2281,"kind":2277},{"path":2285,"title":2018,"kind":2277},{"path":2306,"title":2307,"kind":2277},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-an-entire-row-based-on-a-cell-value-with-openpyxl","Highlight an Entire Row Based on a Cell Value with openpyxl",1790972078086]