[{"data":1,"prerenderedAt":2579},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl":2572},{"id":4,"title":5,"body":6,"dateModified":2549,"datePublished":2549,"description":2550,"extension":2551,"faq":2552,"meta":2563,"navigation":235,"path":2564,"seo":2565,"slug":2568,"stem":2569,"type":2570,"__hash__":2571},"docs\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl\u002Findex.md","Insert and Delete Rows and Columns with openpyxl",{"type":7,"value":8,"toc":2537},"minimark",[9,19,174,179,204,207,367,371,374,499,511,514,724,731,735,741,748,903,914,921,1264,1271,1275,1278,1362,1365,1621,1624,1627,1676,1680,1742,1745,1857,1864,2011,2018,2022,2136,2140,2143,2294,2297,2310,2316,2408,2424,2430,2434,2447,2451,2468,2474,2480,2486,2492,2496,2533],[10,11,12,13,18],"p",{},"Sometimes a sheet needs a new column between two existing ones, or a block of rows removed. openpyxl has four methods for it, and they work — but they do considerably less than Excel's equivalent commands, and the gap is where bugs come from. Formulas do not follow the cells they reference. Merged ranges and conditional formatting do not always move. And deleting rows in the obvious order removes the wrong ones. This guide covers the mechanics and the guard rails. It is part of ",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002F","Using openpyxl for Excel File Manipulation",".",[20,21,30,31,30,35,30,39,30,46,30,53,30,57,30,63,30,72,30,77,30,81,30,84,30,88,30,92,30,95,30,99,30,102,30,108,30,113,30,119,30,124,30,127,30,130,30,132,30,134,30,138,30,142,30,144,30,146,30,148,30,150,30,152,30,154,30,158,30,163,30,168],"svg",{"viewBox":22,"role":23,"ariaLabel":24,"ariaLabelledBy":25,"xmlns":28,"style":29},"0 0 800 244","img","Inserting a row shifts the cells below down, but a SUM formula's text is not rewritten, so it still references the original range and now excludes the new row.",[26,27],"ins-t","ins-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[32,33,34],"title",{"id":26},"What insert_rows moves, and what it leaves behind",[36,37,38],"desc",{"id":27},"Before the insert, rows two to four hold data and row five holds a SUM over B2 to B4. After inserting a row at position three, the data rows shift down and the total lands in row six, but its formula text still reads SUM of B2 to B4. Excel would have rewritten it to B2 to B5; openpyxl does not, so the new row is silently excluded from the total.",[40,41],"rect",{"x":42,"y":42,"width":43,"height":44,"fill":45},"0","800","244","#ffffff",[47,48,52],"text",{"x":49,"y":50,"style":51},"176","28","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","before",[47,54,56],{"x":55,"y":50,"style":51},"600","after insert_rows(3)",[47,58,62],{"x":59,"y":60,"style":61},"46","60","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","2",[40,64],{"x":65,"y":66,"width":67,"height":68,"rx":69,"fill":70,"stroke":71},"64","44","224","26","5","#ebebfd","var(--brand,#5b5cf0)",[47,73,76],{"x":49,"y":74,"style":75},"62","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","North · 120",[47,78,80],{"x":59,"y":79,"style":61},"92","3",[40,82],{"x":65,"y":83,"width":67,"height":68,"rx":69,"fill":70,"stroke":71},"76",[47,85,87],{"x":49,"y":86,"style":75},"94","South · 95",[47,89,91],{"x":59,"y":90,"style":61},"124","4",[40,93],{"x":65,"y":94,"width":67,"height":68,"rx":69,"fill":70,"stroke":71},"108",[47,96,98],{"x":49,"y":97,"style":75},"126","West · 140",[47,100,69],{"x":59,"y":101,"style":61},"156",[40,103],{"x":65,"y":104,"width":67,"height":68,"rx":69,"fill":105,"stroke":106,"style":107},"140","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[47,109,112],{"x":49,"y":110,"style":111},"158","font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","=SUM(B2:B4)",[114,115],"line",{"x1":116,"y1":117,"x2":118,"y2":117,"stroke":71,"style":107},"308","105","348",[120,121],"polygon",{"points":122,"fill":123},"356,105 344,99 344,111","#5b5cf0",[47,125,62],{"x":126,"y":60,"style":61},"470",[40,128],{"x":129,"y":66,"width":67,"height":68,"rx":69,"fill":70,"stroke":71},"488",[47,131,76],{"x":55,"y":74,"style":75},[47,133,80],{"x":126,"y":79,"style":61},[40,135],{"x":129,"y":83,"width":67,"height":68,"rx":69,"fill":136,"stroke":137,"style":107},"#fdefd8","var(--gold,#b4740a)",[47,139,141],{"x":55,"y":86,"style":140},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","East · 88 (new)",[47,143,91],{"x":126,"y":90,"style":61},[40,145],{"x":129,"y":94,"width":67,"height":68,"rx":69,"fill":70,"stroke":71},[47,147,87],{"x":55,"y":97,"style":75},[47,149,69],{"x":126,"y":101,"style":61},[40,151],{"x":129,"y":104,"width":67,"height":68,"rx":69,"fill":70,"stroke":71},[47,153,98],{"x":55,"y":110,"style":75},[47,155,157],{"x":126,"y":156,"style":61},"188","6",[40,159],{"x":129,"y":160,"width":67,"height":68,"rx":69,"fill":161,"stroke":162,"style":107},"172","#fee8f2","var(--accent,#f43f8f)",[47,164,167],{"x":55,"y":165,"style":166},"190","font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","=SUM(B2:B4) — unchanged",[47,169,173],{"x":170,"y":171,"style":172},"400","228","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","the new row is silently excluded from the total",[175,176,178],"h2",{"id":177},"prerequisites","Prerequisites",[180,181,186],"pre",{"className":182,"code":183,"language":184,"meta":185,"style":185},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[187,188,189],"code",{"__ignoreMap":185},[190,191,193,197,201],"span",{"class":114,"line":192},1,[190,194,196],{"class":195},"sMTad","pip",[190,198,200],{"class":199},"srMev"," install",[190,202,203],{"class":199}," openpyxl\n",[10,205,206],{},"A sheet with a formula, so the traps are visible:",[180,208,212],{"className":209,"code":210,"language":211,"meta":185,"style":185},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\n\nwb = Workbook()\nws = wb.active\nws.append([\"region\", \"units\"])\nws.append([\"North\", 120])\nws.append([\"South\", 95])\nws.append([\"West\", 140])\nws[\"A5\"] = \"Total\"\nws[\"B5\"] = \"=SUM(B2:B4)\"\nwb.save(\"sales.xlsx\")\n","python",[187,213,214,230,237,249,260,278,294,309,323,340,355],{"__ignoreMap":185},[190,215,216,220,224,227],{"class":114,"line":192},[190,217,219],{"class":218},"s-kum","from",[190,221,223],{"class":222},"skGVy"," openpyxl ",[190,225,226],{"class":218},"import",[190,228,229],{"class":222}," Workbook\n",[190,231,233],{"class":114,"line":232},2,[190,234,236],{"emptyLinePlaceholder":235},true,"\n",[190,238,240,243,246],{"class":114,"line":239},3,[190,241,242],{"class":222},"wb ",[190,244,245],{"class":218},"=",[190,247,248],{"class":222}," Workbook()\n",[190,250,252,255,257],{"class":114,"line":251},4,[190,253,254],{"class":222},"ws ",[190,256,245],{"class":218},[190,258,259],{"class":222}," wb.active\n",[190,261,263,266,269,272,275],{"class":114,"line":262},5,[190,264,265],{"class":222},"ws.append([",[190,267,268],{"class":199},"\"region\"",[190,270,271],{"class":222},", ",[190,273,274],{"class":199},"\"units\"",[190,276,277],{"class":222},"])\n",[190,279,281,283,286,288,292],{"class":114,"line":280},6,[190,282,265],{"class":222},[190,284,285],{"class":199},"\"North\"",[190,287,271],{"class":222},[190,289,291],{"class":290},"sP0c6","120",[190,293,277],{"class":222},[190,295,297,299,302,304,307],{"class":114,"line":296},7,[190,298,265],{"class":222},[190,300,301],{"class":199},"\"South\"",[190,303,271],{"class":222},[190,305,306],{"class":290},"95",[190,308,277],{"class":222},[190,310,312,314,317,319,321],{"class":114,"line":311},8,[190,313,265],{"class":222},[190,315,316],{"class":199},"\"West\"",[190,318,271],{"class":222},[190,320,104],{"class":290},[190,322,277],{"class":222},[190,324,326,329,332,335,337],{"class":114,"line":325},9,[190,327,328],{"class":222},"ws[",[190,330,331],{"class":199},"\"A5\"",[190,333,334],{"class":222},"] ",[190,336,245],{"class":218},[190,338,339],{"class":199}," \"Total\"\n",[190,341,343,345,348,350,352],{"class":114,"line":342},10,[190,344,328],{"class":222},[190,346,347],{"class":199},"\"B5\"",[190,349,334],{"class":222},[190,351,245],{"class":218},[190,353,354],{"class":199}," \"=SUM(B2:B4)\"\n",[190,356,358,361,364],{"class":114,"line":357},11,[190,359,360],{"class":222},"wb.save(",[190,362,363],{"class":199},"\"sales.xlsx\"",[190,365,366],{"class":222},")\n",[175,368,370],{"id":369},"step-1-insert-rows-and-columns","Step 1 — Insert rows and columns",[10,372,373],{},"The four methods take a position and a count:",[180,375,377],{"className":209,"code":376,"language":211,"meta":185,"style":185},"from openpyxl import load_workbook\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb.active\n\nws.insert_rows(3)                 # one row before row 3\nws.insert_rows(3, amount=2)       # two rows before row 3\nws.insert_cols(2)                 # one column before column B\nws.insert_cols(2, amount=3)       # three columns before column B\n\nwb.save(\"sales_expanded.xlsx\")\n",[187,378,379,390,394,407,415,419,433,455,467,486,490],{"__ignoreMap":185},[190,380,381,383,385,387],{"class":114,"line":192},[190,382,219],{"class":218},[190,384,223],{"class":222},[190,386,226],{"class":218},[190,388,389],{"class":222}," load_workbook\n",[190,391,392],{"class":114,"line":232},[190,393,236],{"emptyLinePlaceholder":235},[190,395,396,398,400,403,405],{"class":114,"line":239},[190,397,242],{"class":222},[190,399,245],{"class":218},[190,401,402],{"class":222}," load_workbook(",[190,404,363],{"class":199},[190,406,366],{"class":222},[190,408,409,411,413],{"class":114,"line":251},[190,410,254],{"class":222},[190,412,245],{"class":218},[190,414,259],{"class":222},[190,416,417],{"class":114,"line":262},[190,418,236],{"emptyLinePlaceholder":235},[190,420,421,424,426,429],{"class":114,"line":280},[190,422,423],{"class":222},"ws.insert_rows(",[190,425,80],{"class":290},[190,427,428],{"class":222},")                 ",[190,430,432],{"class":431},"s-wDw","# one row before row 3\n",[190,434,435,437,439,441,445,447,449,452],{"class":114,"line":296},[190,436,423],{"class":222},[190,438,80],{"class":290},[190,440,271],{"class":222},[190,442,444],{"class":443},"sa561","amount",[190,446,245],{"class":218},[190,448,62],{"class":290},[190,450,451],{"class":222},")       ",[190,453,454],{"class":431},"# two rows before row 3\n",[190,456,457,460,462,464],{"class":114,"line":311},[190,458,459],{"class":222},"ws.insert_cols(",[190,461,62],{"class":290},[190,463,428],{"class":222},[190,465,466],{"class":431},"# one column before column B\n",[190,468,469,471,473,475,477,479,481,483],{"class":114,"line":325},[190,470,459],{"class":222},[190,472,62],{"class":290},[190,474,271],{"class":222},[190,476,444],{"class":443},[190,478,245],{"class":218},[190,480,80],{"class":290},[190,482,451],{"class":222},[190,484,485],{"class":431},"# three columns before column B\n",[190,487,488],{"class":114,"line":342},[190,489,236],{"emptyLinePlaceholder":235},[190,491,492,494,497],{"class":114,"line":357},[190,493,360],{"class":222},[190,495,496],{"class":199},"\"sales_expanded.xlsx\"",[190,498,366],{"class":222},[10,500,501,502,506,507,510],{},"The position is ",[503,504,505],"strong",{},"where the new row will be",", not where it goes after. ",[187,508,509],{},"insert_rows(3)"," makes the new row row 3 and pushes the old row 3 down to row 4.",[10,512,513],{},"Inserted rows are empty and unstyled — they do not inherit the formatting of their neighbours the way Excel's insert does. Fill them yourself:",[180,515,517],{"className":209,"code":516,"language":211,"meta":185,"style":185},"from copy import copy\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb.active\n\nws.insert_rows(3)\nws[\"A3\"] = \"East\"\nws[\"B3\"] = 88\n\n# Copy the style from the row below, which is the old row 3.\nfor col in range(1, ws.max_column + 1):\n    source = ws.cell(row=4, column=col)\n    target = ws.cell(row=3, column=col)\n    target._style = copy(source._style)\n\nwb.save(\"sales_expanded.xlsx\")\n",[187,518,519,531,541,545,557,565,569,577,591,605,609,614,647,675,699,710,715],{"__ignoreMap":185},[190,520,521,523,526,528],{"class":114,"line":192},[190,522,219],{"class":218},[190,524,525],{"class":222}," copy ",[190,527,226],{"class":218},[190,529,530],{"class":222}," copy\n",[190,532,533,535,537,539],{"class":114,"line":232},[190,534,219],{"class":218},[190,536,223],{"class":222},[190,538,226],{"class":218},[190,540,389],{"class":222},[190,542,543],{"class":114,"line":239},[190,544,236],{"emptyLinePlaceholder":235},[190,546,547,549,551,553,555],{"class":114,"line":251},[190,548,242],{"class":222},[190,550,245],{"class":218},[190,552,402],{"class":222},[190,554,363],{"class":199},[190,556,366],{"class":222},[190,558,559,561,563],{"class":114,"line":262},[190,560,254],{"class":222},[190,562,245],{"class":218},[190,564,259],{"class":222},[190,566,567],{"class":114,"line":280},[190,568,236],{"emptyLinePlaceholder":235},[190,570,571,573,575],{"class":114,"line":296},[190,572,423],{"class":222},[190,574,80],{"class":290},[190,576,366],{"class":222},[190,578,579,581,584,586,588],{"class":114,"line":311},[190,580,328],{"class":222},[190,582,583],{"class":199},"\"A3\"",[190,585,334],{"class":222},[190,587,245],{"class":218},[190,589,590],{"class":199}," \"East\"\n",[190,592,593,595,598,600,602],{"class":114,"line":325},[190,594,328],{"class":222},[190,596,597],{"class":199},"\"B3\"",[190,599,334],{"class":222},[190,601,245],{"class":218},[190,603,604],{"class":290}," 88\n",[190,606,607],{"class":114,"line":342},[190,608,236],{"emptyLinePlaceholder":235},[190,610,611],{"class":114,"line":357},[190,612,613],{"class":431},"# Copy the style from the row below, which is the old row 3.\n",[190,615,617,620,623,626,629,632,635,638,641,644],{"class":114,"line":616},12,[190,618,619],{"class":218},"for",[190,621,622],{"class":222}," col ",[190,624,625],{"class":218},"in",[190,627,628],{"class":290}," range",[190,630,631],{"class":222},"(",[190,633,634],{"class":290},"1",[190,636,637],{"class":222},", ws.max_column ",[190,639,640],{"class":218},"+",[190,642,643],{"class":290}," 1",[190,645,646],{"class":222},"):\n",[190,648,650,653,655,658,661,663,665,667,670,672],{"class":114,"line":649},13,[190,651,652],{"class":222},"    source ",[190,654,245],{"class":218},[190,656,657],{"class":222}," ws.cell(",[190,659,660],{"class":443},"row",[190,662,245],{"class":218},[190,664,91],{"class":290},[190,666,271],{"class":222},[190,668,669],{"class":443},"column",[190,671,245],{"class":218},[190,673,674],{"class":222},"col)\n",[190,676,678,681,683,685,687,689,691,693,695,697],{"class":114,"line":677},14,[190,679,680],{"class":222},"    target ",[190,682,245],{"class":218},[190,684,657],{"class":222},[190,686,660],{"class":443},[190,688,245],{"class":218},[190,690,80],{"class":290},[190,692,271],{"class":222},[190,694,669],{"class":443},[190,696,245],{"class":218},[190,698,674],{"class":222},[190,700,702,705,707],{"class":114,"line":701},15,[190,703,704],{"class":222},"    target._style ",[190,706,245],{"class":218},[190,708,709],{"class":222}," copy(source._style)\n",[190,711,713],{"class":114,"line":712},16,[190,714,236],{"emptyLinePlaceholder":235},[190,716,718,720,722],{"class":114,"line":717},17,[190,719,360],{"class":222},[190,721,496],{"class":199},[190,723,366],{"class":222},[10,725,726,727,730],{},"Copying ",[187,728,729],{},"_style"," wholesale carries font, fill, border, alignment and number format in one assignment. It is a private attribute, but it is the practical way to clone a cell's complete appearance — assigning the individual style objects one by one is both longer and easy to leave incomplete.",[175,732,734],{"id":733},"step-2-fix-the-formulas-yourself","Step 2 — Fix the formulas yourself",[10,736,737,738,740],{},"This is the part that catches people. openpyxl moves cells; it does not rewrite formula text. After the insert above, the total still reads ",[187,739,112],{}," and excludes the new row.",[10,742,743,744,747],{},"You have two options. The robust one is to ",[503,745,746],{},"rewrite the formulas after any structural change",", which is easy when your script owns the layout:",[180,749,751],{"className":209,"code":750,"language":211,"meta":185,"style":185},"from openpyxl import load_workbook\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb.active\n\nws.insert_rows(3)\nws[\"A3\"], ws[\"B3\"] = \"East\", 88\n\n# Rewrite the total to span whatever the data range now is.\nlast_data_row = ws.max_row - 1          # the total sits on the last row\nws.cell(row=ws.max_row, column=2).value = f\"=SUM(B2:B{last_data_row})\"\n\nwb.save(\"sales_expanded.xlsx\")\n",[187,752,753,763,767,779,787,791,799,822,826,831,849,891,895],{"__ignoreMap":185},[190,754,755,757,759,761],{"class":114,"line":192},[190,756,219],{"class":218},[190,758,223],{"class":222},[190,760,226],{"class":218},[190,762,389],{"class":222},[190,764,765],{"class":114,"line":232},[190,766,236],{"emptyLinePlaceholder":235},[190,768,769,771,773,775,777],{"class":114,"line":239},[190,770,242],{"class":222},[190,772,245],{"class":218},[190,774,402],{"class":222},[190,776,363],{"class":199},[190,778,366],{"class":222},[190,780,781,783,785],{"class":114,"line":251},[190,782,254],{"class":222},[190,784,245],{"class":218},[190,786,259],{"class":222},[190,788,789],{"class":114,"line":262},[190,790,236],{"emptyLinePlaceholder":235},[190,792,793,795,797],{"class":114,"line":280},[190,794,423],{"class":222},[190,796,80],{"class":290},[190,798,366],{"class":222},[190,800,801,803,805,808,810,812,814,817,819],{"class":114,"line":296},[190,802,328],{"class":222},[190,804,583],{"class":199},[190,806,807],{"class":222},"], ws[",[190,809,597],{"class":199},[190,811,334],{"class":222},[190,813,245],{"class":218},[190,815,816],{"class":199}," \"East\"",[190,818,271],{"class":222},[190,820,821],{"class":290},"88\n",[190,823,824],{"class":114,"line":311},[190,825,236],{"emptyLinePlaceholder":235},[190,827,828],{"class":114,"line":325},[190,829,830],{"class":431},"# Rewrite the total to span whatever the data range now is.\n",[190,832,833,836,838,841,844,846],{"class":114,"line":342},[190,834,835],{"class":222},"last_data_row ",[190,837,245],{"class":218},[190,839,840],{"class":222}," ws.max_row ",[190,842,843],{"class":218},"-",[190,845,643],{"class":290},[190,847,848],{"class":431},"          # the total sits on the last row\n",[190,850,851,854,856,858,861,863,865,867,870,872,875,878,882,885,888],{"class":114,"line":357},[190,852,853],{"class":222},"ws.cell(",[190,855,660],{"class":443},[190,857,245],{"class":218},[190,859,860],{"class":222},"ws.max_row, ",[190,862,669],{"class":443},[190,864,245],{"class":218},[190,866,62],{"class":290},[190,868,869],{"class":222},").value ",[190,871,245],{"class":218},[190,873,874],{"class":218}," f",[190,876,877],{"class":199},"\"=SUM(B2:B",[190,879,881],{"class":880},"sSjpA","{",[190,883,884],{"class":222},"last_data_row",[190,886,887],{"class":880},"}",[190,889,890],{"class":199},")\"\n",[190,892,893],{"class":114,"line":616},[190,894,236],{"emptyLinePlaceholder":235},[190,896,897,899,901],{"class":114,"line":649},[190,898,360],{"class":222},[190,900,496],{"class":199},[190,902,366],{"class":222},[10,904,905,906,909,910,18],{},"The more durable one is to ",[503,907,908],{},"use a named range"," for the data block and have the formula reference the name. Names are stored once and are far easier to update than scanning formula text — see ",[14,911,913],{"href":912},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fcreate-named-range-in-excel-with-openpyxl\u002F","creating a named range in Excel with openpyxl",[10,915,916,917,920],{},"If you must patch existing formulas, do it deliberately rather than with a blanket regular expression — a naive substitution will happily corrupt a text cell that happens to contain something like ",[187,918,919],{},"B2:B4",":",[180,922,924],{"className":209,"code":923,"language":211,"meta":185,"style":185},"import re\nfrom openpyxl import load_workbook\n\nRANGE = re.compile(r\"\\b([A-Z]{1,3})(\\d+):([A-Z]{1,3})(\\d+)\\b\")\n\ndef shift_ranges(formula, at_row, amount):\n    \"\"\"Extend ranges that span the insertion point. Formulas only.\"\"\"\n    def repl(m):\n        c1, r1, c2, r2 = m.group(1), int(m.group(2)), m.group(3), int(m.group(4))\n        if r1 \u003C at_row \u003C= r2:\n            r2 += amount\n        return f\"{c1}{r1}:{c2}{r2}\"\n    return RANGE.sub(repl, formula)\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb.active\nfor row in ws.iter_rows():\n    for cell in row:\n        if isinstance(cell.value, str) and cell.value.startswith(\"=\"):\n            cell.value = shift_ranges(cell.value, at_row=3, amount=1)\n",[187,925,926,933,943,947,1006,1010,1022,1027,1038,1077,1097,1108,1147,1158,1162,1174,1182,1194,1208,1236],{"__ignoreMap":185},[190,927,928,930],{"class":114,"line":192},[190,929,226],{"class":218},[190,931,932],{"class":222}," re\n",[190,934,935,937,939,941],{"class":114,"line":232},[190,936,219],{"class":218},[190,938,223],{"class":222},[190,940,226],{"class":218},[190,942,389],{"class":222},[190,944,945],{"class":114,"line":239},[190,946,236],{"emptyLinePlaceholder":235},[190,948,949,952,955,958,961,964,967,970,973,976,979,981,984,986,989,991,993,995,997,999,1002,1004],{"class":114,"line":251},[190,950,951],{"class":290},"RANGE",[190,953,954],{"class":218}," =",[190,956,957],{"class":222}," re.compile(",[190,959,960],{"class":218},"r",[190,962,963],{"class":199},"\"",[190,965,966],{"class":290},"\\b([",[190,968,969],{"class":880},"A-Z",[190,971,972],{"class":290},"]",[190,974,975],{"class":218},"{1,3}",[190,977,978],{"class":290},")(\\d",[190,980,640],{"class":218},[190,982,983],{"class":290},")",[190,985,920],{"class":199},[190,987,988],{"class":290},"([",[190,990,969],{"class":880},[190,992,972],{"class":290},[190,994,975],{"class":218},[190,996,978],{"class":290},[190,998,640],{"class":218},[190,1000,1001],{"class":290},")\\b",[190,1003,963],{"class":199},[190,1005,366],{"class":222},[190,1007,1008],{"class":114,"line":262},[190,1009,236],{"emptyLinePlaceholder":235},[190,1011,1012,1015,1019],{"class":114,"line":280},[190,1013,1014],{"class":218},"def",[190,1016,1018],{"class":1017},"s_Opv"," shift_ranges",[190,1020,1021],{"class":222},"(formula, at_row, amount):\n",[190,1023,1024],{"class":114,"line":296},[190,1025,1026],{"class":199},"    \"\"\"Extend ranges that span the insertion point. Formulas only.\"\"\"\n",[190,1028,1029,1032,1035],{"class":114,"line":311},[190,1030,1031],{"class":218},"    def",[190,1033,1034],{"class":1017}," repl",[190,1036,1037],{"class":222},"(m):\n",[190,1039,1040,1043,1045,1048,1050,1053,1056,1059,1061,1064,1066,1068,1070,1072,1074],{"class":114,"line":325},[190,1041,1042],{"class":222},"        c1, r1, c2, r2 ",[190,1044,245],{"class":218},[190,1046,1047],{"class":222}," m.group(",[190,1049,634],{"class":290},[190,1051,1052],{"class":222},"), ",[190,1054,1055],{"class":290},"int",[190,1057,1058],{"class":222},"(m.group(",[190,1060,62],{"class":290},[190,1062,1063],{"class":222},")), m.group(",[190,1065,80],{"class":290},[190,1067,1052],{"class":222},[190,1069,1055],{"class":290},[190,1071,1058],{"class":222},[190,1073,91],{"class":290},[190,1075,1076],{"class":222},"))\n",[190,1078,1079,1082,1085,1088,1091,1094],{"class":114,"line":342},[190,1080,1081],{"class":218},"        if",[190,1083,1084],{"class":222}," r1 ",[190,1086,1087],{"class":218},"\u003C",[190,1089,1090],{"class":222}," at_row ",[190,1092,1093],{"class":218},"\u003C=",[190,1095,1096],{"class":222}," r2:\n",[190,1098,1099,1102,1105],{"class":114,"line":357},[190,1100,1101],{"class":222},"            r2 ",[190,1103,1104],{"class":218},"+=",[190,1106,1107],{"class":222}," amount\n",[190,1109,1110,1113,1115,1117,1119,1122,1125,1128,1130,1132,1134,1137,1139,1142,1144],{"class":114,"line":616},[190,1111,1112],{"class":218},"        return",[190,1114,874],{"class":218},[190,1116,963],{"class":199},[190,1118,881],{"class":880},[190,1120,1121],{"class":222},"c1",[190,1123,1124],{"class":880},"}{",[190,1126,1127],{"class":222},"r1",[190,1129,887],{"class":880},[190,1131,920],{"class":199},[190,1133,881],{"class":880},[190,1135,1136],{"class":222},"c2",[190,1138,1124],{"class":880},[190,1140,1141],{"class":222},"r2",[190,1143,887],{"class":880},[190,1145,1146],{"class":199},"\"\n",[190,1148,1149,1152,1155],{"class":114,"line":649},[190,1150,1151],{"class":218},"    return",[190,1153,1154],{"class":290}," RANGE",[190,1156,1157],{"class":222},".sub(repl, formula)\n",[190,1159,1160],{"class":114,"line":677},[190,1161,236],{"emptyLinePlaceholder":235},[190,1163,1164,1166,1168,1170,1172],{"class":114,"line":701},[190,1165,242],{"class":222},[190,1167,245],{"class":218},[190,1169,402],{"class":222},[190,1171,363],{"class":199},[190,1173,366],{"class":222},[190,1175,1176,1178,1180],{"class":114,"line":712},[190,1177,254],{"class":222},[190,1179,245],{"class":218},[190,1181,259],{"class":222},[190,1183,1184,1186,1189,1191],{"class":114,"line":717},[190,1185,619],{"class":218},[190,1187,1188],{"class":222}," row ",[190,1190,625],{"class":218},[190,1192,1193],{"class":222}," ws.iter_rows():\n",[190,1195,1197,1200,1203,1205],{"class":114,"line":1196},18,[190,1198,1199],{"class":218},"    for",[190,1201,1202],{"class":222}," cell ",[190,1204,625],{"class":218},[190,1206,1207],{"class":222}," row:\n",[190,1209,1211,1213,1216,1219,1222,1225,1228,1231,1234],{"class":114,"line":1210},19,[190,1212,1081],{"class":218},[190,1214,1215],{"class":290}," isinstance",[190,1217,1218],{"class":222},"(cell.value, ",[190,1220,1221],{"class":290},"str",[190,1223,1224],{"class":222},") ",[190,1226,1227],{"class":218},"and",[190,1229,1230],{"class":222}," cell.value.startswith(",[190,1232,1233],{"class":199},"\"=\"",[190,1235,646],{"class":222},[190,1237,1239,1242,1244,1247,1250,1252,1254,1256,1258,1260,1262],{"class":114,"line":1238},20,[190,1240,1241],{"class":222},"            cell.value ",[190,1243,245],{"class":218},[190,1245,1246],{"class":222}," shift_ranges(cell.value, ",[190,1248,1249],{"class":443},"at_row",[190,1251,245],{"class":218},[190,1253,80],{"class":290},[190,1255,271],{"class":222},[190,1257,444],{"class":443},[190,1259,245],{"class":218},[190,1261,634],{"class":290},[190,1263,366],{"class":222},[10,1265,1266,1267,1270],{},"The ",[187,1268,1269],{},"startswith(\"=\")"," guard is what keeps this safe — only formula cells are touched.",[175,1272,1274],{"id":1273},"step-3-delete-in-reverse-order","Step 3 — Delete in reverse order",[10,1276,1277],{},"Deleting looks symmetrical and is not. Every deletion shifts the rows below it up by one, so a list of row numbers collected beforehand goes stale the moment you delete the first.",[20,1279,30,1285,30,1288,30,1291,30,1294,30,1297,30,1303,30,1309,30,1314,30,1318,30,1322,30,1326,30,1331,30,1335,30,1338,30,1343,30,1346,30,1349,30,1352,30,1355,30,1358],{"viewBox":1280,"role":23,"ariaLabel":1281,"ariaLabelledBy":1282,"xmlns":28,"style":29},"0 0 800 250","Deleting rows three and five: ascending order removes row three then what is now row five, which was originally row six, whereas descending order removes exactly the intended rows.",[1283,1284],"del-t","del-d",[32,1286,1287],{"id":1283},"Why deleting rows in ascending order removes the wrong ones",[36,1289,1290],{"id":1284},"Starting from six rows, the goal is to delete rows three and five. Deleting ascending removes row three first, which shifts everything below up by one, so the second deletion of row five actually removes what was originally row six — the wrong row, with no error. Deleting descending removes row five first, which does not affect the position of row three, so the second deletion removes exactly the intended row.",[40,1292],{"x":42,"y":42,"width":43,"height":1293,"fill":45},"250",[47,1295,1296],{"x":170,"y":68,"style":51},"goal: delete rows 3 and 5",[40,1298],{"x":1299,"y":1300,"width":1301,"height":1302,"rx":1299,"fill":161,"stroke":162,"style":107},"14","42","368","192",[47,1304,1308],{"x":1305,"y":1306,"style":1307},"198","68","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","ascending: 3 then 5",[47,1310,1313],{"x":1305,"y":1311,"style":1312},"98","font-size:11px;fill:var(--text,#172033);text-anchor:middle","delete 3 → rows 4,5,6 shift up",[47,1315,1317],{"x":1305,"y":1316,"style":1312},"122","to become 3,4,5",[47,1319,1321],{"x":1305,"y":1320,"style":1312},"152","delete 5 → removes what",[47,1323,1325],{"x":1305,"y":1324,"style":1312},"174","was originally row 6",[40,1327],{"x":1300,"y":165,"width":1328,"height":1329,"rx":1330,"fill":45,"stroke":162},"312","32","8",[47,1332,1334],{"x":1305,"y":1333,"style":172},"211","wrong row gone, no error raised",[40,1336],{"x":1337,"y":1300,"width":1301,"height":1302,"rx":1299,"fill":105,"stroke":106,"style":107},"418",[47,1339,1342],{"x":1340,"y":1306,"style":1341},"602","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","descending: 5 then 3",[47,1344,1345],{"x":1340,"y":1311,"style":1312},"delete 5 → only rows below",[47,1347,1348],{"x":1340,"y":1316,"style":1312},"row 5 move",[47,1350,1351],{"x":1340,"y":1320,"style":1312},"delete 3 → row 3 is still",[47,1353,1354],{"x":1340,"y":1324,"style":1312},"exactly where it was",[40,1356],{"x":1357,"y":165,"width":1328,"height":1329,"rx":1330,"fill":45,"stroke":106},"446",[47,1359,1361],{"x":1340,"y":1333,"style":1360},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","both intended rows removed",[10,1363,1364],{},"Collect first, delete second, and always descending:",[180,1366,1368],{"className":209,"code":1367,"language":211,"meta":185,"style":185},"from openpyxl import load_workbook\n\ndef delete_rows_where(ws, predicate, min_row=2):\n    \"\"\"Delete every row for which predicate(row_values) is true.\"\"\"\n    doomed = [\n        cell.row\n        for cell in (r[0] for r in ws.iter_rows(min_row=min_row))\n        if predicate([c.value for c in ws[cell.row]])\n    ]\n\n    # Descending: deleting a later row never moves an earlier one.\n    for row in sorted(doomed, reverse=True):\n        ws.delete_rows(row)\n\n    return len(doomed)\n\nwb = load_workbook(\"sales.xlsx\")\nws = wb.active\nremoved = delete_rows_where(ws, lambda values: values[1] in (None, 0))\nprint(f\"removed {removed} rows\")\nwb.save(\"sales_clean.xlsx\")\n",[187,1369,1370,1380,1384,1400,1405,1415,1420,1454,1471,1476,1480,1485,1509,1514,1518,1528,1532,1544,1552,1586,1611],{"__ignoreMap":185},[190,1371,1372,1374,1376,1378],{"class":114,"line":192},[190,1373,219],{"class":218},[190,1375,223],{"class":222},[190,1377,226],{"class":218},[190,1379,389],{"class":222},[190,1381,1382],{"class":114,"line":232},[190,1383,236],{"emptyLinePlaceholder":235},[190,1385,1386,1388,1391,1394,1396,1398],{"class":114,"line":239},[190,1387,1014],{"class":218},[190,1389,1390],{"class":1017}," delete_rows_where",[190,1392,1393],{"class":222},"(ws, predicate, min_row",[190,1395,245],{"class":218},[190,1397,62],{"class":290},[190,1399,646],{"class":222},[190,1401,1402],{"class":114,"line":251},[190,1403,1404],{"class":199},"    \"\"\"Delete every row for which predicate(row_values) is true.\"\"\"\n",[190,1406,1407,1410,1412],{"class":114,"line":262},[190,1408,1409],{"class":222},"    doomed ",[190,1411,245],{"class":218},[190,1413,1414],{"class":222}," [\n",[190,1416,1417],{"class":114,"line":280},[190,1418,1419],{"class":222},"        cell.row\n",[190,1421,1422,1425,1427,1429,1432,1434,1436,1438,1441,1443,1446,1449,1451],{"class":114,"line":296},[190,1423,1424],{"class":218},"        for",[190,1426,1202],{"class":222},[190,1428,625],{"class":218},[190,1430,1431],{"class":222}," (r[",[190,1433,42],{"class":290},[190,1435,334],{"class":222},[190,1437,619],{"class":218},[190,1439,1440],{"class":222}," r ",[190,1442,625],{"class":218},[190,1444,1445],{"class":222}," ws.iter_rows(",[190,1447,1448],{"class":443},"min_row",[190,1450,245],{"class":218},[190,1452,1453],{"class":222},"min_row))\n",[190,1455,1456,1458,1461,1463,1466,1468],{"class":114,"line":311},[190,1457,1081],{"class":218},[190,1459,1460],{"class":222}," predicate([c.value ",[190,1462,619],{"class":218},[190,1464,1465],{"class":222}," c ",[190,1467,625],{"class":218},[190,1469,1470],{"class":222}," ws[cell.row]])\n",[190,1472,1473],{"class":114,"line":325},[190,1474,1475],{"class":222},"    ]\n",[190,1477,1478],{"class":114,"line":342},[190,1479,236],{"emptyLinePlaceholder":235},[190,1481,1482],{"class":114,"line":357},[190,1483,1484],{"class":431},"    # Descending: deleting a later row never moves an earlier one.\n",[190,1486,1487,1489,1491,1493,1496,1499,1502,1504,1507],{"class":114,"line":616},[190,1488,1199],{"class":218},[190,1490,1188],{"class":222},[190,1492,625],{"class":218},[190,1494,1495],{"class":290}," sorted",[190,1497,1498],{"class":222},"(doomed, ",[190,1500,1501],{"class":443},"reverse",[190,1503,245],{"class":218},[190,1505,1506],{"class":290},"True",[190,1508,646],{"class":222},[190,1510,1511],{"class":114,"line":649},[190,1512,1513],{"class":222},"        ws.delete_rows(row)\n",[190,1515,1516],{"class":114,"line":677},[190,1517,236],{"emptyLinePlaceholder":235},[190,1519,1520,1522,1525],{"class":114,"line":701},[190,1521,1151],{"class":218},[190,1523,1524],{"class":290}," len",[190,1526,1527],{"class":222},"(doomed)\n",[190,1529,1530],{"class":114,"line":712},[190,1531,236],{"emptyLinePlaceholder":235},[190,1533,1534,1536,1538,1540,1542],{"class":114,"line":717},[190,1535,242],{"class":222},[190,1537,245],{"class":218},[190,1539,402],{"class":222},[190,1541,363],{"class":199},[190,1543,366],{"class":222},[190,1545,1546,1548,1550],{"class":114,"line":1196},[190,1547,254],{"class":222},[190,1549,245],{"class":218},[190,1551,259],{"class":222},[190,1553,1554,1557,1559,1562,1565,1568,1570,1572,1574,1577,1580,1582,1584],{"class":114,"line":1210},[190,1555,1556],{"class":222},"removed ",[190,1558,245],{"class":218},[190,1560,1561],{"class":222}," delete_rows_where(ws, ",[190,1563,1564],{"class":218},"lambda",[190,1566,1567],{"class":222}," values: values[",[190,1569,634],{"class":290},[190,1571,334],{"class":222},[190,1573,625],{"class":218},[190,1575,1576],{"class":222}," (",[190,1578,1579],{"class":290},"None",[190,1581,271],{"class":222},[190,1583,42],{"class":290},[190,1585,1076],{"class":222},[190,1587,1588,1591,1593,1596,1599,1601,1604,1606,1609],{"class":114,"line":1238},[190,1589,1590],{"class":290},"print",[190,1592,631],{"class":222},[190,1594,1595],{"class":218},"f",[190,1597,1598],{"class":199},"\"removed ",[190,1600,881],{"class":880},[190,1602,1603],{"class":222},"removed",[190,1605,887],{"class":880},[190,1607,1608],{"class":199}," rows\"",[190,1610,366],{"class":222},[190,1612,1614,1616,1619],{"class":114,"line":1613},21,[190,1615,360],{"class":222},[190,1617,1618],{"class":199},"\"sales_clean.xlsx\"",[190,1620,366],{"class":222},[10,1622,1623],{},"Never delete while iterating. The iterator holds positions that the deletion invalidates, and you get skipped rows or an exception with no clear cause.",[10,1625,1626],{},"Deleting contiguous blocks is both faster and simpler — one call instead of many:",[180,1628,1630],{"className":209,"code":1629,"language":211,"meta":185,"style":185},"ws.delete_rows(10, amount=25)        # rows 10 through 34 in one operation\nws.delete_cols(4, amount=3)          # columns D, E and F\n",[187,1631,1632,1655],{"__ignoreMap":185},[190,1633,1634,1637,1640,1642,1644,1646,1649,1652],{"class":114,"line":192},[190,1635,1636],{"class":222},"ws.delete_rows(",[190,1638,1639],{"class":290},"10",[190,1641,271],{"class":222},[190,1643,444],{"class":443},[190,1645,245],{"class":218},[190,1647,1648],{"class":290},"25",[190,1650,1651],{"class":222},")        ",[190,1653,1654],{"class":431},"# rows 10 through 34 in one operation\n",[190,1656,1657,1660,1662,1664,1666,1668,1670,1673],{"class":114,"line":232},[190,1658,1659],{"class":222},"ws.delete_cols(",[190,1661,91],{"class":290},[190,1663,271],{"class":222},[190,1665,444],{"class":443},[190,1667,245],{"class":218},[190,1669,80],{"class":290},[190,1671,1672],{"class":222},")          ",[190,1674,1675],{"class":431},"# columns D, E and F\n",[175,1677,1679],{"id":1678},"step-4-repair-what-did-not-move","Step 4 — Repair what did not move",[20,1681,30,1687,30,1690,30,1693,30,1696,30,1699,30,1704,30,1709,30,1713,30,1717,30,1721,30,1723,30,1727,30,1730,30,1733,30,1736,30,1739],{"viewBox":1682,"role":23,"ariaLabel":1683,"ariaLabelledBy":1684,"xmlns":28,"style":29},"0 0 800 234","What openpyxl moves on an insert and what it leaves at fixed coordinates: cell values and styles shift, while merged ranges, conditional formatting and chart references stay put.",[1685,1686],"moves-t","moves-d",[32,1688,1689],{"id":1685},"What shifts with an insert, and what stays behind",[36,1691,1692],{"id":1686},"Two columns. Cell values, cell styles and row heights move with the rows, which is the behaviour people expect. Formula text, merged ranges, conditional formatting ranges, data validation ranges and chart series references are stored as coordinates and are not all recalculated, so they end up pointing at the wrong cells. The second group must be checked and re-applied after any structural change.",[40,1694],{"x":42,"y":42,"width":43,"height":1695,"fill":45},"234",[40,1697],{"x":1299,"y":1698,"width":1301,"height":1302,"rx":1299,"fill":105,"stroke":106,"style":107},"24",[47,1700,1703],{"x":1305,"y":1701,"style":1702},"52","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","moves with the rows",[47,1705,1708],{"x":1305,"y":1706,"style":1707},"84","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","cell values",[47,1710,1712],{"x":1305,"y":1711,"style":1707},"110","cell styles and number formats",[47,1714,1716],{"x":1305,"y":1715,"style":1707},"136","row heights",[47,1718,1720],{"x":1305,"y":1719,"style":1360},"180","the behaviour you expect",[40,1722],{"x":1337,"y":1698,"width":1301,"height":1302,"rx":1299,"fill":161,"stroke":162,"style":107},[47,1724,1726],{"x":1340,"y":1701,"style":1725},"font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","stays at fixed coordinates",[47,1728,1729],{"x":1340,"y":1706,"style":1707},"formula text inside cells",[47,1731,1732],{"x":1340,"y":1711,"style":1707},"merged ranges",[47,1734,1735],{"x":1340,"y":1715,"style":1707},"conditional formatting ranges",[47,1737,1738],{"x":1340,"y":110,"style":1707},"validation and chart references",[47,1740,1741],{"x":1340,"y":1302,"style":172},"check and re-apply after any insert or delete",[10,1743,1744],{},"Merged ranges, conditional formatting, data validation and chart references are stored as coordinate strings, and openpyxl does not consistently rewrite all of them when the sheet shifts. Check afterwards:",[180,1746,1748],{"className":209,"code":1747,"language":211,"meta":185,"style":185},"from openpyxl import load_workbook\n\nwb = load_workbook(\"sales_expanded.xlsx\")\nws = wb.active\n\nprint(\"merged:\", [str(r) for r in ws.merged_cells.ranges])\nprint(\"validations:\", [str(dv.sqref) for dv in ws.data_validations.dataValidation])\nprint(\"conditional:\", list(ws.conditional_formatting))\n",[187,1749,1750,1760,1764,1776,1784,1788,1814,1840],{"__ignoreMap":185},[190,1751,1752,1754,1756,1758],{"class":114,"line":192},[190,1753,219],{"class":218},[190,1755,223],{"class":222},[190,1757,226],{"class":218},[190,1759,389],{"class":222},[190,1761,1762],{"class":114,"line":232},[190,1763,236],{"emptyLinePlaceholder":235},[190,1765,1766,1768,1770,1772,1774],{"class":114,"line":239},[190,1767,242],{"class":222},[190,1769,245],{"class":218},[190,1771,402],{"class":222},[190,1773,496],{"class":199},[190,1775,366],{"class":222},[190,1777,1778,1780,1782],{"class":114,"line":251},[190,1779,254],{"class":222},[190,1781,245],{"class":218},[190,1783,259],{"class":222},[190,1785,1786],{"class":114,"line":262},[190,1787,236],{"emptyLinePlaceholder":235},[190,1789,1790,1792,1794,1797,1800,1802,1805,1807,1809,1811],{"class":114,"line":280},[190,1791,1590],{"class":290},[190,1793,631],{"class":222},[190,1795,1796],{"class":199},"\"merged:\"",[190,1798,1799],{"class":222},", [",[190,1801,1221],{"class":290},[190,1803,1804],{"class":222},"(r) ",[190,1806,619],{"class":218},[190,1808,1440],{"class":222},[190,1810,625],{"class":218},[190,1812,1813],{"class":222}," ws.merged_cells.ranges])\n",[190,1815,1816,1818,1820,1823,1825,1827,1830,1832,1835,1837],{"class":114,"line":296},[190,1817,1590],{"class":290},[190,1819,631],{"class":222},[190,1821,1822],{"class":199},"\"validations:\"",[190,1824,1799],{"class":222},[190,1826,1221],{"class":290},[190,1828,1829],{"class":222},"(dv.sqref) ",[190,1831,619],{"class":218},[190,1833,1834],{"class":222}," dv ",[190,1836,625],{"class":218},[190,1838,1839],{"class":222}," ws.data_validations.dataValidation])\n",[190,1841,1842,1844,1846,1849,1851,1854],{"class":114,"line":311},[190,1843,1590],{"class":290},[190,1845,631],{"class":222},[190,1847,1848],{"class":199},"\"conditional:\"",[190,1850,271],{"class":222},[190,1852,1853],{"class":290},"list",[190,1855,1856],{"class":222},"(ws.conditional_formatting))\n",[10,1858,1859,1860,1863],{},"The pragmatic pattern for a sheet with several of these is to ",[503,1861,1862],{},"strip and re-apply"," rather than trying to patch coordinates. Re-adding a conditional format over the new range is a few lines and is guaranteed correct, whereas patching a range string is guesswork:",[180,1865,1867],{"className":209,"code":1866,"language":211,"meta":185,"style":185},"from openpyxl.formatting.rule import CellIsRule\nfrom openpyxl.styles import PatternFill\n\nws.conditional_formatting = type(ws.conditional_formatting)()   # clear\nlast = ws.max_row - 1\nws.conditional_formatting.add(\n    f\"B2:B{last}\",\n    CellIsRule(operator=\"lessThan\", formula=[\"100\"],\n               fill=PatternFill(\"solid\", bgColor=\"FEE8F2\")),\n)\n",[187,1868,1869,1881,1893,1897,1913,1927,1932,1952,1981,2007],{"__ignoreMap":185},[190,1870,1871,1873,1876,1878],{"class":114,"line":192},[190,1872,219],{"class":218},[190,1874,1875],{"class":222}," openpyxl.formatting.rule ",[190,1877,226],{"class":218},[190,1879,1880],{"class":222}," CellIsRule\n",[190,1882,1883,1885,1888,1890],{"class":114,"line":232},[190,1884,219],{"class":218},[190,1886,1887],{"class":222}," openpyxl.styles ",[190,1889,226],{"class":218},[190,1891,1892],{"class":222}," PatternFill\n",[190,1894,1895],{"class":114,"line":239},[190,1896,236],{"emptyLinePlaceholder":235},[190,1898,1899,1902,1904,1907,1910],{"class":114,"line":251},[190,1900,1901],{"class":222},"ws.conditional_formatting ",[190,1903,245],{"class":218},[190,1905,1906],{"class":290}," type",[190,1908,1909],{"class":222},"(ws.conditional_formatting)()   ",[190,1911,1912],{"class":431},"# clear\n",[190,1914,1915,1918,1920,1922,1924],{"class":114,"line":262},[190,1916,1917],{"class":222},"last ",[190,1919,245],{"class":218},[190,1921,840],{"class":222},[190,1923,843],{"class":218},[190,1925,1926],{"class":290}," 1\n",[190,1928,1929],{"class":114,"line":280},[190,1930,1931],{"class":222},"ws.conditional_formatting.add(\n",[190,1933,1934,1937,1940,1942,1945,1947,1949],{"class":114,"line":296},[190,1935,1936],{"class":218},"    f",[190,1938,1939],{"class":199},"\"B2:B",[190,1941,881],{"class":880},[190,1943,1944],{"class":222},"last",[190,1946,887],{"class":880},[190,1948,963],{"class":199},[190,1950,1951],{"class":222},",\n",[190,1953,1954,1957,1960,1962,1965,1967,1970,1972,1975,1978],{"class":114,"line":311},[190,1955,1956],{"class":222},"    CellIsRule(",[190,1958,1959],{"class":443},"operator",[190,1961,245],{"class":218},[190,1963,1964],{"class":199},"\"lessThan\"",[190,1966,271],{"class":222},[190,1968,1969],{"class":443},"formula",[190,1971,245],{"class":218},[190,1973,1974],{"class":222},"[",[190,1976,1977],{"class":199},"\"100\"",[190,1979,1980],{"class":222},"],\n",[190,1982,1983,1986,1988,1991,1994,1996,1999,2001,2004],{"class":114,"line":325},[190,1984,1985],{"class":443},"               fill",[190,1987,245],{"class":218},[190,1989,1990],{"class":222},"PatternFill(",[190,1992,1993],{"class":199},"\"solid\"",[190,1995,271],{"class":222},[190,1997,1998],{"class":443},"bgColor",[190,2000,245],{"class":218},[190,2002,2003],{"class":199},"\"FEE8F2\"",[190,2005,2006],{"class":222},")),\n",[190,2008,2009],{"class":114,"line":342},[190,2010,366],{"class":222},[10,2012,2013,2014,18],{},"The same reasoning applies to the rules described in ",[14,2015,2017],{"href":2016},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fopenpyxl-apply-conditional-formatting-to-range\u002F","applying conditional formatting to a range with openpyxl",[175,2019,2021],{"id":2020},"common-pitfalls-and-fixes","Common pitfalls and fixes",[2023,2024,2025,2041],"table",{},[2026,2027,2028],"thead",{},[2029,2030,2031,2035,2038],"tr",{},[2032,2033,2034],"th",{},"Symptom",[2032,2036,2037],{},"Cause",[2032,2039,2040],{},"Fix",[2042,2043,2044,2056,2067,2078,2092,2103,2114,2125],"tbody",{},[2029,2045,2046,2050,2053],{},[2047,2048,2049],"td",{},"Total excludes the new row",[2047,2051,2052],{},"Formula text not rewritten",[2047,2054,2055],{},"Rewrite the formula, or use a named range.",[2029,2057,2058,2061,2064],{},[2047,2059,2060],{},"Wrong rows deleted",[2047,2062,2063],{},"Deleted in ascending order",[2047,2065,2066],{},"Sort the row numbers descending.",[2029,2068,2069,2072,2075],{},[2047,2070,2071],{},"Iterator skips rows or raises",[2047,2073,2074],{},"Deleting during iteration",[2047,2076,2077],{},"Collect first, delete in a second pass.",[2029,2079,2080,2083,2086],{},[2047,2081,2082],{},"Inserted row has no formatting",[2047,2084,2085],{},"openpyxl does not inherit styles",[2047,2087,2088,2089,2091],{},"Copy ",[187,2090,729],{}," from a neighbouring row.",[2029,2093,2094,2097,2100],{},[2047,2095,2096],{},"Merged range now covers the wrong cells",[2047,2098,2099],{},"Merges stored as coordinates",[2047,2101,2102],{},"Re-apply merges after the change.",[2029,2104,2105,2108,2111],{},[2047,2106,2107],{},"Conditional formatting stops at the old last row",[2047,2109,2110],{},"Range string unchanged",[2047,2112,2113],{},"Clear and re-add the rule.",[2029,2115,2116,2119,2122],{},[2047,2117,2118],{},"Chart plots the wrong series",[2047,2120,2121],{},"Chart references are absolute",[2047,2123,2124],{},"Rebuild the chart after restructuring.",[2029,2126,2127,2130,2133],{},[2047,2128,2129],{},"Very slow with many inserts",[2047,2131,2132],{},"Every insert shifts all cells below",[2047,2134,2135],{},"Rebuild the sheet instead.",[175,2137,2139],{"id":2138},"performance-and-scale-notes","Performance and scale notes",[10,2141,2142],{},"Each insert or delete shifts every cell below the point of change. That makes a single call cheap and a loop of calls quadratic — a thousand individual inserts on a fifty-thousand-row sheet moves cells fifty million times.",[180,2144,2146],{"className":209,"code":2145,"language":211,"meta":185,"style":185},"import time\nfrom openpyxl import Workbook\n\nwb = Workbook(); ws = wb.active\nfor i in range(20_000):\n    ws.append([f\"row {i}\", i])\n\nstart = time.perf_counter()\nfor _ in range(200):\n    ws.insert_rows(2)\nprint(f\"200 inserts: {time.perf_counter() - start:.2f}s\")\n",[187,2147,2148,2155,2165,2169,2182,2200,2222,2226,2236,2254,2263],{"__ignoreMap":185},[190,2149,2150,2152],{"class":114,"line":192},[190,2151,226],{"class":218},[190,2153,2154],{"class":222}," time\n",[190,2156,2157,2159,2161,2163],{"class":114,"line":232},[190,2158,219],{"class":218},[190,2160,223],{"class":222},[190,2162,226],{"class":218},[190,2164,229],{"class":222},[190,2166,2167],{"class":114,"line":239},[190,2168,236],{"emptyLinePlaceholder":235},[190,2170,2171,2173,2175,2178,2180],{"class":114,"line":251},[190,2172,242],{"class":222},[190,2174,245],{"class":218},[190,2176,2177],{"class":222}," Workbook(); ws ",[190,2179,245],{"class":218},[190,2181,259],{"class":222},[190,2183,2184,2186,2189,2191,2193,2195,2198],{"class":114,"line":262},[190,2185,619],{"class":218},[190,2187,2188],{"class":222}," i ",[190,2190,625],{"class":218},[190,2192,628],{"class":290},[190,2194,631],{"class":222},[190,2196,2197],{"class":290},"20_000",[190,2199,646],{"class":222},[190,2201,2202,2205,2207,2210,2212,2215,2217,2219],{"class":114,"line":280},[190,2203,2204],{"class":222},"    ws.append([",[190,2206,1595],{"class":218},[190,2208,2209],{"class":199},"\"row ",[190,2211,881],{"class":880},[190,2213,2214],{"class":222},"i",[190,2216,887],{"class":880},[190,2218,963],{"class":199},[190,2220,2221],{"class":222},", i])\n",[190,2223,2224],{"class":114,"line":296},[190,2225,236],{"emptyLinePlaceholder":235},[190,2227,2228,2231,2233],{"class":114,"line":311},[190,2229,2230],{"class":222},"start ",[190,2232,245],{"class":218},[190,2234,2235],{"class":222}," time.perf_counter()\n",[190,2237,2238,2240,2243,2245,2247,2249,2252],{"class":114,"line":325},[190,2239,619],{"class":218},[190,2241,2242],{"class":222}," _ ",[190,2244,625],{"class":218},[190,2246,628],{"class":290},[190,2248,631],{"class":222},[190,2250,2251],{"class":290},"200",[190,2253,646],{"class":222},[190,2255,2256,2259,2261],{"class":114,"line":342},[190,2257,2258],{"class":222},"    ws.insert_rows(",[190,2260,62],{"class":290},[190,2262,366],{"class":222},[190,2264,2265,2267,2269,2271,2274,2276,2279,2281,2284,2287,2289,2292],{"class":114,"line":357},[190,2266,1590],{"class":290},[190,2268,631],{"class":222},[190,2270,1595],{"class":218},[190,2272,2273],{"class":199},"\"200 inserts: ",[190,2275,881],{"class":880},[190,2277,2278],{"class":222},"time.perf_counter() ",[190,2280,843],{"class":218},[190,2282,2283],{"class":222}," start",[190,2285,2286],{"class":218},":.2f",[190,2288,887],{"class":880},[190,2290,2291],{"class":199},"s\"",[190,2293,366],{"class":222},[10,2295,2296],{},"Three rules follow.",[10,2298,2299,2305,2306,2309],{},[503,2300,2301,2302,18],{},"Batch with ",[187,2303,2304],{},"amount="," One ",[187,2307,2308],{},"insert_rows(2, amount=200)"," does the shifting once; two hundred separate calls do it two hundred times.",[10,2311,2312,2315],{},[503,2313,2314],{},"Rebuild rather than restructure"," when the change is substantial. Writing the rows you want into a fresh sheet is linear, and it sidesteps every formula, merge and conditional-formatting problem in this guide:",[180,2317,2319],{"className":209,"code":2318,"language":211,"meta":185,"style":185},"import pandas as pd\n\n# Instead of deleting a thousand rows one by one:\ndf = pd.read_excel(\"sales.xlsx\")\ndf = df[df[\"units\"] > 0]\ndf.to_excel(\"sales_clean.xlsx\", index=False, engine=\"xlsxwriter\")\n",[187,2320,2321,2334,2338,2343,2357,2379],{"__ignoreMap":185},[190,2322,2323,2325,2328,2331],{"class":114,"line":192},[190,2324,226],{"class":218},[190,2326,2327],{"class":222}," pandas ",[190,2329,2330],{"class":218},"as",[190,2332,2333],{"class":222}," pd\n",[190,2335,2336],{"class":114,"line":232},[190,2337,236],{"emptyLinePlaceholder":235},[190,2339,2340],{"class":114,"line":239},[190,2341,2342],{"class":431},"# Instead of deleting a thousand rows one by one:\n",[190,2344,2345,2348,2350,2353,2355],{"class":114,"line":251},[190,2346,2347],{"class":222},"df ",[190,2349,245],{"class":218},[190,2351,2352],{"class":222}," pd.read_excel(",[190,2354,363],{"class":199},[190,2356,366],{"class":222},[190,2358,2359,2361,2363,2366,2368,2370,2373,2376],{"class":114,"line":262},[190,2360,2347],{"class":222},[190,2362,245],{"class":218},[190,2364,2365],{"class":222}," df[df[",[190,2367,274],{"class":199},[190,2369,334],{"class":222},[190,2371,2372],{"class":218},">",[190,2374,2375],{"class":290}," 0",[190,2377,2378],{"class":222},"]\n",[190,2380,2381,2384,2386,2388,2391,2393,2396,2398,2401,2403,2406],{"class":114,"line":280},[190,2382,2383],{"class":222},"df.to_excel(",[190,2385,1618],{"class":199},[190,2387,271],{"class":222},[190,2389,2390],{"class":443},"index",[190,2392,245],{"class":218},[190,2394,2395],{"class":290},"False",[190,2397,271],{"class":222},[190,2399,2400],{"class":443},"engine",[190,2402,245],{"class":218},[190,2404,2405],{"class":199},"\"xlsxwriter\"",[190,2407,366],{"class":222},[10,2409,2410,2411,2414,2415,2418,2419,2423],{},"For most cleaning tasks that is not just faster but simpler — the row filtering belongs in pandas, and the workbook is written once at the end. Reach for ",[187,2412,2413],{},"insert_rows"," and ",[187,2416,2417],{},"delete_rows"," when you must preserve an existing sheet's formatting and structure, as when filling a template; otherwise let ",[14,2420,2422],{"href":2421},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002F","writing a DataFrame to Excel"," produce the shape you want directly.",[10,2425,2426,2429],{},[503,2427,2428],{},"Do the structural work before the styling."," Inserting rows into a heavily styled sheet moves style references as well as values, so a script that styles first and restructures afterwards does the expensive work twice. Get the shape right, then format.",[175,2431,2433],{"id":2432},"conclusion","Conclusion",[10,2435,2436,271,2438,271,2441,2414,2443,2446],{},[187,2437,2413],{},[187,2439,2440],{},"insert_cols",[187,2442,2417],{},[187,2444,2445],{},"delete_cols"," move cells and nothing else. Formulas keep their original text, so rewrite them — or reference a named range that you update once. Delete in descending order, always, and collect the row numbers in a separate pass from the deletion. Check merged ranges, conditional formatting and validations afterwards, and re-apply rather than patch. And when the change is more than a few operations, rebuilding the sheet from the data you want is both faster and free of every trap above.",[175,2448,2450],{"id":2449},"frequently-asked-questions","Frequently asked questions",[10,2452,2453,2456,2457,2460,2461,2464,2465,2467],{},[503,2454,2455],{},"Do formulas update when I insert a row with openpyxl?","\nNo. openpyxl moves cell values and styles but does not rewrite formula text, so a ",[187,2458,2459],{},"SUM"," over ",[187,2462,2463],{},"B2:B10"," still says ",[187,2466,2463],{}," after a row is inserted in the middle. Excel would adjust it; openpyxl does not.",[10,2469,2470,2473],{},[503,2471,2472],{},"Why did my deletion remove the wrong rows?","\nYou deleted in ascending order. Every deletion shifts the rows below up by one, so the indexes you collected before starting no longer point at the same rows. Delete from the bottom upwards instead.",[10,2475,2476,2479],{},[503,2477,2478],{},"Do merged cells, charts and conditional formatting move with the rows?","\nNot reliably. Merged ranges, chart references, data validation ranges and conditional formatting ranges are stored as coordinates and are not all recalculated on insert or delete. Check and re-apply them afterwards.",[10,2481,2482,2485],{},[503,2483,2484],{},"Is it faster to rebuild the sheet than to insert rows?","\nAlmost always, for anything beyond a handful of operations. Each insert or delete shifts every cell below the point of change, so a loop of a thousand inserts is far slower than writing a new sheet from the desired data.",[10,2487,2488,2491],{},[503,2489,2490],{},"How do I delete every row matching a condition?","\nCollect the matching row numbers in one pass, then delete them in reverse order in a second pass. Never delete while iterating — the iterator and the sheet fall out of step.",[175,2493,2495],{"id":2494},"related","Related",[2497,2498,2499,2506,2513,2519,2526],"ul",{},[2500,2501,2502,2503,2505],"li",{},"Up to the parent: ",[14,2504,17],{"href":16}," — the wider toolkit.",[2500,2507,2508,2512],{},[14,2509,2511],{"href":2510},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fiterate-over-rows-and-columns-with-openpyxl\u002F","Iterate over Rows and Columns with openpyxl"," — the read pass that feeds the deletion list.",[2500,2514,2515,2518],{},[14,2516,2517],{"href":912},"Create a Named Range in Excel with openpyxl"," — formulas that survive restructuring.",[2500,2520,2521,2525],{},[14,2522,2524],{"href":2523},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Fremove-blank-rows-from-excel-with-pandas\u002F","Remove Blank Rows from Excel with pandas"," — the rebuild-instead approach.",[2500,2527,2528,2532],{},[14,2529,2531],{"href":2530},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Frename-reorder-and-delete-excel-sheets-with-openpyxl\u002F","Rename, Reorder and Delete Excel Sheets with openpyxl"," — the same operations at sheet level.",[2534,2535,2536],"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 .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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":185,"searchDepth":232,"depth":232,"links":2538},[2539,2540,2541,2542,2543,2544,2545,2546,2547,2548],{"id":177,"depth":232,"text":178},{"id":369,"depth":232,"text":370},{"id":733,"depth":232,"text":734},{"id":1273,"depth":232,"text":1274},{"id":1678,"depth":232,"text":1679},{"id":2020,"depth":232,"text":2021},{"id":2138,"depth":232,"text":2139},{"id":2432,"depth":232,"text":2433},{"id":2449,"depth":232,"text":2450},{"id":2494,"depth":232,"text":2495},"2026-08-15","Add and remove rows and columns in an existing Excel file with openpyxl — insert_rows, delete_cols, why formulas and merges do not move, and safe deletion in reverse order.","md",[2553,2555,2557,2559,2561],{"q":2455,"a":2554},"No. openpyxl moves cell values and styles but does not rewrite formula text, so a SUM over B2 to B10 still says B2 to B10 after a row is inserted in the middle. Excel would adjust it; openpyxl does not.",{"q":2472,"a":2556},"You deleted in ascending order. Every deletion shifts the rows below up by one, so the indexes you collected before starting no longer point at the same rows. Delete from the bottom upwards instead.",{"q":2478,"a":2558},"Not reliably. Merged ranges, chart references, data validation ranges and conditional formatting ranges are stored as coordinates and are not all recalculated on insert or delete. Check and re-apply them afterwards.",{"q":2484,"a":2560},"Almost always, for anything beyond a handful of operations. Each insert or delete shifts every cell below the point of change, so a loop of a thousand inserts is far slower than writing a new sheet from the desired data.",{"q":2490,"a":2562},"Collect the matching row numbers in one pass, then delete them in reverse order in a second pass. Never delete while iterating — the iterator and the sheet fall out of step.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl",{"title":2566,"description":2567},"openpyxl Insert and Delete Rows and Columns","How to insert_rows, insert_cols, delete_rows and delete_cols in openpyxl, why formula references and merged ranges break, and the reverse-order rule for safe deletion.","insert-and-delete-rows-and-columns-with-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl\u002Findex","how-to","t4BWwnggZiNgAs9Hgy7RA3xfFSPCFic4MCuZzoqcn94",[2573,2576],{"title":17,"path":2574,"stem":2575,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Findex",{"title":2511,"path":2577,"stem":2578,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fiterate-over-rows-and-columns-with-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fiterate-over-rows-and-columns-with-openpyxl\u002Findex",1786800028495]