[{"data":1,"prerenderedAt":1909},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows":1873,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows":1882,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows":1887,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows":1888},{"id":4,"title":5,"body":6,"dateModified":1838,"datePublished":1838,"description":1839,"extension":1840,"faq":1841,"keyword":1850,"kind":1851,"meta":1852,"navigation":316,"path":1861,"seo":1862,"slug":1865,"stem":1866,"topics":1867,"type":1871,"__hash__":1872},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows\u002Findex.md","Fix openpyxl max_row Counting Empty Rows",{"type":7,"value":8,"toc":1824},"minimark",[9,13,24,41,241,246,271,278,282,288,477,480,523,526,627,631,637,809,823,975,979,988,1020,1023,1181,1193,1197,1203,1303,1322,1326,1333,1387,1394,1534,1540,1622,1626,1629,1668,1679,1683,1726,1730,1741,1745,1759,1763,1769,1775,1781,1787,1791,1820],[10,11,12],"p",{},"A worksheet has a header and five rows of data, but openpyxl disagrees:",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"language-text",">>> ws.max_row\n2000\n","text","",[22,23,18],"code",{"__ignoreMap":20},[10,25,26,27,30,31,34,35,40],{},"The symptom that usually brings people here is not the number itself but what follows from it: ",[22,28,29],{},"ws.append()"," writes the new row at 2001, two thousand rows below the table, and the next person to open the report scrolls past a blank sheet to find it. ",[22,32,33],{},"max_row"," is not broken. It answers a different question than the one you are asking — \"what is the highest row that has a cell object?\", not \"where does the data end?\" This page, part of ",[36,37,39],"a",{"href":38},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",", shows what creates those cell objects and how to get the answer you actually want.",[42,43,51,52,51,56,51,60,51,68,51,74,51,83,51,87,51,93,51,97,51,101,51,105,51,110,51,112,51,116,51,118,51,121,51,124,51,128,51,130,51,133,51,135,51,138,51,141,51,145,51,150,51,154,51,156,51,159,51,162,51,166,51,170,51,174,51,176,51,179,51,182,51,184,51,188,51,192,51,200,51,205,51,211,51,222,51,228,51,233,51,237],"svg",{"viewBox":44,"role":45,"ariaLabelledBy":46,"xmlns":49,"style":50},"135 0 490 228","img",[47,48],"mr-used-t","mr-used-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:490px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[53,54,55],"title",{"id":47},"Rows that count toward max_row without holding data",[57,58,59],"desc",{"id":48},"A worksheet with a header and five data rows. Rows 7 to 1000 are empty but bold-formatted, and B2000 contains an empty string. All of them create cell objects, so max_row reports 2000 while the data ends at row 6.",[61,62],"rect",{"x":63,"y":64,"width":65,"height":66,"fill":67},"135","0","490","228","#ffffff",[19,69,73],{"x":70,"y":71,"style":72},"380.0","31.0","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What openpyxl sees in ledger.xlsx",[61,75],{"x":76,"y":77,"width":78,"height":79,"rx":64,"fill":80,"stroke":81,"style":82},"151.4","48.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[61,84],{"x":85,"y":77,"width":86,"height":79,"rx":64,"fill":80,"stroke":81,"style":82},"183.4","106.9",[19,88,92],{"x":89,"y":90,"style":91},"236.9","65.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[61,94],{"x":95,"y":77,"width":96,"height":79,"rx":64,"fill":80,"stroke":81,"style":82},"290.3","70.9",[19,98,100],{"x":99,"y":90,"style":91},"325.7","B",[61,102],{"x":76,"y":103,"width":78,"height":104,"rx":64,"fill":80,"stroke":81,"style":82},"72.0","28.0",[19,106,109],{"x":107,"y":108,"style":91},"167.4","91.0","1",[61,111],{"x":85,"y":103,"width":86,"height":104,"rx":64,"fill":67,"stroke":81,"style":82},[19,113,115],{"x":89,"y":108,"style":114},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","id",[61,117],{"x":95,"y":103,"width":96,"height":104,"rx":64,"fill":67,"stroke":81,"style":82},[19,119,120],{"x":99,"y":108,"style":114},"amount",[61,122],{"x":76,"y":123,"width":78,"height":104,"rx":64,"fill":80,"stroke":81,"style":82},"100.0",[19,125,127],{"x":107,"y":126,"style":91},"119.0","2",[61,129],{"x":85,"y":123,"width":86,"height":104,"rx":64,"fill":67,"stroke":81,"style":82},[19,131,109],{"x":89,"y":126,"style":132},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[61,134],{"x":95,"y":123,"width":96,"height":104,"rx":64,"fill":67,"stroke":81,"style":82},[19,136,137],{"x":99,"y":126,"style":132},"10",[61,139],{"x":76,"y":140,"width":78,"height":104,"rx":64,"fill":80,"stroke":81,"style":82},"128.0",[19,142,144],{"x":107,"y":143,"style":91},"147.0","3",[61,146],{"x":85,"y":140,"width":86,"height":104,"rx":64,"fill":147,"stroke":148,"style":149},"#d9f4f1","var(--teal,#0f9488)","stroke-width:1.5px",[19,151,153],{"x":89,"y":143,"style":152},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","5",[61,155],{"x":95,"y":140,"width":96,"height":104,"rx":64,"fill":147,"stroke":148,"style":149},[19,157,158],{"x":99,"y":143,"style":152},"50",[61,160],{"x":76,"y":161,"width":78,"height":104,"rx":64,"fill":80,"stroke":81,"style":82},"156.0",[19,163,165],{"x":107,"y":164,"style":91},"175.0","4",[61,167],{"x":85,"y":161,"width":86,"height":104,"rx":64,"fill":168,"stroke":169,"style":149},"#fdefd8","var(--gold,#b4740a)",[19,171,173],{"x":89,"y":164,"style":172},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","(bold, empty)",[61,175],{"x":95,"y":161,"width":96,"height":104,"rx":64,"fill":67,"stroke":81,"style":82},[61,177],{"x":76,"y":178,"width":78,"height":104,"rx":64,"fill":80,"stroke":81,"style":82},"184.0",[19,180,153],{"x":107,"y":181,"style":91},"203.0",[61,183],{"x":85,"y":178,"width":86,"height":104,"rx":64,"fill":67,"stroke":81,"style":82},[61,185],{"x":95,"y":178,"width":96,"height":104,"rx":64,"fill":186,"stroke":187,"style":149},"#fee8f2","var(--accent,#f43f8f)",[19,189,191],{"x":99,"y":181,"style":190},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","\"\"",[193,194],"line",{"x1":195,"y1":196,"x2":197,"y2":196,"stroke":198,"style":199},"369.1","130.0","401.1","var(--brand,#5b5cf0)","stroke-width:2px",[201,202],"polygon",{"points":203,"fill":204},"405.1,130.0 396.1,135.0 396.1,125.0","#5b5cf0",[61,206],{"x":207,"y":208,"width":209,"height":140,"rx":210,"fill":168,"stroke":169,"style":199},"411.1","66.0","197.4","12",[212,213,215],"g",{"transform":214},"translate(497.9,79.0) scale(1.000)",[216,217],"path",{"d":218,"fill":219,"stroke":220,"style":221},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","none","var(--gold-ink,#7a4e06)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[19,223,227],{"x":224,"y":225,"style":226},"509.9","125.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","max_row = 2000",[19,229,232],{"x":224,"y":230,"style":231},"145.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","data ends at row 6;",[19,234,236],{"x":224,"y":235,"style":231},"162.5","formatting runs to 1000;",[19,238,240],{"x":224,"y":239,"style":231},"180.0","an empty string sits at B2000",[242,243,245],"h2",{"id":244},"prerequisites","Prerequisites",[14,247,251],{"className":248,"code":249,"language":250,"meta":20,"style":20},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash",[22,252,253],{"__ignoreMap":20},[254,255,257,261,265,268],"span",{"class":193,"line":256},1,[254,258,260],{"class":259},"sMTad","pip",[254,262,264],{"class":263},"srMev"," install",[254,266,267],{"class":263}," openpyxl",[254,269,270],{"class":263}," pandas\n",[10,272,273,274,277],{},"The examples use ",[22,275,276],{},"ledger.xlsx",": six real rows, bold formatting applied down to row 1,000, and an empty string in B2000 — a fair model of a sheet someone formatted by selecting whole columns.",[242,279,281],{"id":280},"step-1-see-what-max_row-is-counting","Step 1 — See what max_row is counting",[10,283,284,285,287],{},"Load the file and compare ",[22,286,33],{}," with the rows that hold values:",[14,289,293],{"className":290,"code":291,"language":292,"meta":20,"style":20},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\n\nwb = load_workbook(\"ledger.xlsx\")\nws = wb.active\nprint(ws.max_row, ws.dimensions)\n# 2000 A1:B2000\n\nhas_value = [r for r in range(1, ws.max_row + 1)\n             if any(c.value not in (None, \"\") for c in ws[r])]\nprint(len(has_value), has_value[-1])\n# 6 6\n","python",[22,294,295,311,318,336,347,357,364,369,408,450,471],{"__ignoreMap":20},[254,296,297,301,305,308],{"class":193,"line":256},[254,298,300],{"class":299},"s-kum","from",[254,302,304],{"class":303},"skGVy"," openpyxl ",[254,306,307],{"class":299},"import",[254,309,310],{"class":303}," load_workbook\n",[254,312,314],{"class":193,"line":313},2,[254,315,317],{"emptyLinePlaceholder":316},true,"\n",[254,319,321,324,327,330,333],{"class":193,"line":320},3,[254,322,323],{"class":303},"wb ",[254,325,326],{"class":299},"=",[254,328,329],{"class":303}," load_workbook(",[254,331,332],{"class":263},"\"ledger.xlsx\"",[254,334,335],{"class":303},")\n",[254,337,339,342,344],{"class":193,"line":338},4,[254,340,341],{"class":303},"ws ",[254,343,326],{"class":299},[254,345,346],{"class":303}," wb.active\n",[254,348,350,354],{"class":193,"line":349},5,[254,351,353],{"class":352},"sP0c6","print",[254,355,356],{"class":303},"(ws.max_row, ws.dimensions)\n",[254,358,360],{"class":193,"line":359},6,[254,361,363],{"class":362},"s-wDw","# 2000 A1:B2000\n",[254,365,367],{"class":193,"line":366},7,[254,368,317],{"emptyLinePlaceholder":316},[254,370,372,375,377,380,383,386,389,392,395,397,400,403,406],{"class":193,"line":371},8,[254,373,374],{"class":303},"has_value ",[254,376,326],{"class":299},[254,378,379],{"class":303}," [r ",[254,381,382],{"class":299},"for",[254,384,385],{"class":303}," r ",[254,387,388],{"class":299},"in",[254,390,391],{"class":352}," range",[254,393,394],{"class":303},"(",[254,396,109],{"class":352},[254,398,399],{"class":303},", ws.max_row ",[254,401,402],{"class":299},"+",[254,404,405],{"class":352}," 1",[254,407,335],{"class":303},[254,409,411,414,417,420,423,426,429,432,435,437,440,442,445,447],{"class":193,"line":410},9,[254,412,413],{"class":299},"             if",[254,415,416],{"class":352}," any",[254,418,419],{"class":303},"(c.value ",[254,421,422],{"class":299},"not",[254,424,425],{"class":299}," in",[254,427,428],{"class":303}," (",[254,430,431],{"class":352},"None",[254,433,434],{"class":303},", ",[254,436,191],{"class":263},[254,438,439],{"class":303},") ",[254,441,382],{"class":299},[254,443,444],{"class":303}," c ",[254,446,388],{"class":299},[254,448,449],{"class":303}," ws[r])]\n",[254,451,453,455,457,460,463,466,468],{"class":193,"line":452},10,[254,454,353],{"class":352},[254,456,394],{"class":303},[254,458,459],{"class":352},"len",[254,461,462],{"class":303},"(has_value), has_value[",[254,464,465],{"class":299},"-",[254,467,109],{"class":352},[254,469,470],{"class":303},"])\n",[254,472,474],{"class":193,"line":473},11,[254,475,476],{"class":362},"# 6 6\n",[10,478,479],{},"Three kinds of cell inflate the count:",[481,482,483,495,504],"ol",{},[484,485,486,490,491,494],"li",{},[487,488,489],"strong",{},"Formatted empty cells."," Selecting a whole column in Excel and making it bold, or giving a range borders \"ready for data\", stores a style on every one of those cells. openpyxl loads them as real cells with ",[22,492,493],{},"value=None",".",[484,496,497,500,501,503],{},[487,498,499],{},"Cells holding an empty string."," An exported ",[22,502,191],{}," is a value as far as the file is concerned. So is a single space.",[484,505,506,509,510,514,515,518,519,522],{},[487,507,508],{},"Cells your own script created."," In normal (non-read-only) mode, ",[511,512,513],"em",{},"reading"," ",[22,516,517],{},"ws.cell(row, col)"," or ",[22,520,521],{},"ws[\"A5000\"]"," creates the cell if it did not exist. A loop that probes rows to find the end of the data moves the end as it goes.",[10,524,525],{},"The third one deserves a demonstration, because it makes code that looks read-only change the sheet:",[14,527,529],{"className":290,"code":528,"language":292,"meta":20,"style":20},"from openpyxl import Workbook\n\nprobe = Workbook().active\nprobe.append([\"only row\"])\nfor r in range(1, 101):\n    probe.cell(row=r, column=1).value      # just looking\nprint(probe.max_row)\n# 100\n",[22,530,531,542,546,556,566,588,615,622],{"__ignoreMap":20},[254,532,533,535,537,539],{"class":193,"line":256},[254,534,300],{"class":299},[254,536,304],{"class":303},[254,538,307],{"class":299},[254,540,541],{"class":303}," Workbook\n",[254,543,544],{"class":193,"line":313},[254,545,317],{"emptyLinePlaceholder":316},[254,547,548,551,553],{"class":193,"line":320},[254,549,550],{"class":303},"probe ",[254,552,326],{"class":299},[254,554,555],{"class":303}," Workbook().active\n",[254,557,558,561,564],{"class":193,"line":338},[254,559,560],{"class":303},"probe.append([",[254,562,563],{"class":263},"\"only row\"",[254,565,470],{"class":303},[254,567,568,570,572,574,576,578,580,582,585],{"class":193,"line":349},[254,569,382],{"class":299},[254,571,385],{"class":303},[254,573,388],{"class":299},[254,575,391],{"class":352},[254,577,394],{"class":303},[254,579,109],{"class":352},[254,581,434],{"class":303},[254,583,584],{"class":352},"101",[254,586,587],{"class":303},"):\n",[254,589,590,593,597,599,602,605,607,609,612],{"class":193,"line":359},[254,591,592],{"class":303},"    probe.cell(",[254,594,596],{"class":595},"sa561","row",[254,598,326],{"class":299},[254,600,601],{"class":303},"r, ",[254,603,604],{"class":595},"column",[254,606,326],{"class":299},[254,608,109],{"class":352},[254,610,611],{"class":303},").value      ",[254,613,614],{"class":362},"# just looking\n",[254,616,617,619],{"class":193,"line":366},[254,618,353],{"class":352},[254,620,621],{"class":303},"(probe.max_row)\n",[254,623,624],{"class":193,"line":371},[254,625,626],{"class":362},"# 100\n",[242,628,630],{"id":629},"step-2-find-the-real-last-row","Step 2 — Find the real last row",[10,632,633,634,636],{},"Walk upward from ",[22,635,33],{}," and stop at the first row with a real value. Walking up is much faster than walking down when the inflation is large, because it stops as soon as it reaches the data:",[14,638,640],{"className":290,"code":639,"language":292,"meta":20,"style":20},"def last_data_row(ws, column=None):\n    \"\"\"Highest row with a non-empty value (in one column, or in any column).\"\"\"\n    for row in range(ws.max_row, 0, -1):\n        cells = [ws.cell(row=row, column=column)] if column else ws[row]\n        if any(c.value is not None and str(c.value).strip() != \"\" for c in cells):\n            return row\n    return 0\n\nprint(last_data_row(ws), last_data_row(ws, column=2))\n# 6 6\n",[22,641,642,660,665,690,726,769,777,785,789,805],{"__ignoreMap":20},[254,643,644,647,651,654,656,658],{"class":193,"line":256},[254,645,646],{"class":299},"def",[254,648,650],{"class":649},"s_Opv"," last_data_row",[254,652,653],{"class":303},"(ws, column",[254,655,326],{"class":299},[254,657,431],{"class":352},[254,659,587],{"class":303},[254,661,662],{"class":193,"line":313},[254,663,664],{"class":263},"    \"\"\"Highest row with a non-empty value (in one column, or in any column).\"\"\"\n",[254,666,667,670,673,675,677,680,682,684,686,688],{"class":193,"line":320},[254,668,669],{"class":299},"    for",[254,671,672],{"class":303}," row ",[254,674,388],{"class":299},[254,676,391],{"class":352},[254,678,679],{"class":303},"(ws.max_row, ",[254,681,64],{"class":352},[254,683,434],{"class":303},[254,685,465],{"class":299},[254,687,109],{"class":352},[254,689,587],{"class":303},[254,691,692,695,697,700,702,704,707,709,711,714,717,720,723],{"class":193,"line":338},[254,693,694],{"class":303},"        cells ",[254,696,326],{"class":299},[254,698,699],{"class":303}," [ws.cell(",[254,701,596],{"class":595},[254,703,326],{"class":299},[254,705,706],{"class":303},"row, ",[254,708,604],{"class":595},[254,710,326],{"class":299},[254,712,713],{"class":303},"column)] ",[254,715,716],{"class":299},"if",[254,718,719],{"class":303}," column ",[254,721,722],{"class":299},"else",[254,724,725],{"class":303}," ws[row]\n",[254,727,728,731,733,735,738,741,744,747,750,753,756,759,762,764,766],{"class":193,"line":349},[254,729,730],{"class":299},"        if",[254,732,416],{"class":352},[254,734,419],{"class":303},[254,736,737],{"class":299},"is",[254,739,740],{"class":299}," not",[254,742,743],{"class":352}," None",[254,745,746],{"class":299}," and",[254,748,749],{"class":352}," str",[254,751,752],{"class":303},"(c.value).strip() ",[254,754,755],{"class":299},"!=",[254,757,758],{"class":263}," \"\"",[254,760,761],{"class":299}," for",[254,763,444],{"class":303},[254,765,388],{"class":299},[254,767,768],{"class":303}," cells):\n",[254,770,771,774],{"class":193,"line":359},[254,772,773],{"class":299},"            return",[254,775,776],{"class":303}," row\n",[254,778,779,782],{"class":193,"line":366},[254,780,781],{"class":299},"    return",[254,783,784],{"class":352}," 0\n",[254,786,787],{"class":193,"line":371},[254,788,317],{"emptyLinePlaceholder":316},[254,790,791,793,796,798,800,802],{"class":193,"line":410},[254,792,353],{"class":352},[254,794,795],{"class":303},"(last_data_row(ws), last_data_row(ws, ",[254,797,604],{"class":595},[254,799,326],{"class":299},[254,801,127],{"class":352},[254,803,804],{"class":303},"))\n",[254,806,807],{"class":193,"line":452},[254,808,476],{"class":362},[10,810,811,812,815,816,818,819,822],{},"Treating whitespace-only strings as empty is a deliberate choice for report data. If a single space is meaningful in your sheet, drop the ",[22,813,814],{},"strip()",". Passing a ",[22,817,604],{}," is useful when one key column is always filled — an ID or a date — because it ignores stray notes typed further right. ",[22,820,821],{},"ws.cell"," inside this loop still creates cells above row 6, but only in rows that are already inside the inflated range, so it does not make things worse.",[42,824,51,830,51,833,51,836,51,840,51,846,51,853,51,859,51,862,51,866,51,871,51,874,51,877,51,883,51,887,51,891,51,894,51,898,51,901,51,904,51,910,51,913,51,915,51,918,51,922,51,925,51,928,51,935,51,940,51,943,51,946,51,950,51,953,51,957,51,964,51,969,51,972],{"viewBox":825,"role":45,"ariaLabelledBy":826,"xmlns":49,"style":829},"4 0 752 142",[827,828],"mr-walk-t","mr-walk-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[53,831,832],{"id":827},"Finding the last data row by walking upward",[57,834,835],{"id":828},"Start at max_row, which is 2000. Check each row for a non-empty value, moving upward. Rows 2000 down to 7 are empty or blank strings and are skipped. Row 6 holds values, so the function returns 6 and the next write goes to row 7.",[61,837],{"x":165,"y":64,"width":838,"height":839,"fill":67},"752","142",[61,841],{"x":842,"y":843,"width":844,"height":845,"rx":210,"fill":80,"stroke":81,"style":199},"20.0","16.0","115.2","110.5",[212,847,849],{"transform":848},"translate(65.6,29.0) scale(1.000)",[216,850],{"d":851,"fill":219,"stroke":852,"style":221},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--text,#172033)",[19,854,858],{"x":855,"y":856,"style":857},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Start at",[19,860,33],{"x":855,"y":861,"style":857},"93.0",[19,863,865],{"x":855,"y":864,"style":231},"112.5","row 2000",[193,867],{"x1":868,"y1":869,"x2":870,"y2":869,"stroke":198,"style":199},"140.2","71.2","162.2",[201,872],{"points":873,"fill":204},"166.2,71.2 157.2,76.2 157.2,66.2",[61,875],{"x":876,"y":843,"width":844,"height":845,"rx":210,"fill":168,"stroke":169,"style":199},"171.2",[212,878,880],{"transform":879},"translate(216.8,29.0) scale(1.000)",[216,881],{"d":882,"fill":219,"stroke":220,"style":221},"M3 5h18l-7 8v6l-4 2v-8z",[19,884,886],{"x":885,"y":856,"style":226},"228.8","Row empty?",[19,888,890],{"x":885,"y":889,"style":231},"95.0","None, \"\" or",[19,892,893],{"x":885,"y":864,"style":231},"spaces",[193,895],{"x1":896,"y1":869,"x2":897,"y2":869,"stroke":198,"style":199},"291.4","313.4",[201,899],{"points":900,"fill":204},"317.4,71.2 308.4,76.2 308.4,66.2",[61,902],{"x":903,"y":843,"width":844,"height":845,"rx":210,"fill":80,"stroke":81,"style":199},"322.4",[212,905,907],{"transform":906},"translate(368.0,29.0) scale(1.000)",[216,908],{"d":909,"fill":219,"stroke":852,"style":221},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[19,911,912],{"x":70,"y":856,"style":857},"Move up one",[19,914,596],{"x":70,"y":861,"style":857},[19,916,917],{"x":70,"y":864,"style":231},"1999, 1998 …",[193,919],{"x1":920,"y1":869,"x2":921,"y2":869,"stroke":198,"style":199},"442.6","464.6",[201,923],{"points":924,"fill":204},"468.6,71.2 459.6,76.2 459.6,66.2",[61,926],{"x":927,"y":843,"width":844,"height":845,"rx":210,"fill":147,"stroke":148,"style":199},"473.6",[212,929,931],{"transform":930},"translate(519.2,29.0) scale(1.000)",[216,932],{"d":933,"fill":219,"stroke":934,"style":221},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[19,936,939],{"x":937,"y":856,"style":938},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","First real",[19,941,942],{"x":937,"y":861,"style":938},"value",[19,944,945],{"x":937,"y":864,"style":231},"row 6 — stop",[193,947],{"x1":948,"y1":869,"x2":949,"y2":869,"stroke":198,"style":199},"593.8","615.8",[201,951],{"points":952,"fill":204},"619.8,71.2 610.8,76.2 610.8,66.2",[61,954],{"x":955,"y":843,"width":844,"height":845,"rx":210,"fill":956,"stroke":198,"style":199},"624.8","#f0f4ff",[212,958,960],{"transform":959},"translate(670.4,29.0) scale(1.000)",[216,961],{"d":962,"fill":219,"stroke":963,"style":221},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--brand-strong,#4338ca)",[19,965,968],{"x":966,"y":856,"style":967},"682.4","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Write at last",[19,970,971],{"x":966,"y":861,"style":967},"+ 1",[19,973,974],{"x":966,"y":864,"style":231},"row 7",[242,976,978],{"id":977},"step-3-append-where-the-data-ends","Step 3 — Append where the data ends",[10,980,981,983,984,987],{},[22,982,29],{}," has no notion of \"data\"; it writes to ",[22,985,986],{},"max_row + 1",". On the inflated sheet that is row 2001:",[14,989,991],{"className":290,"code":990,"language":292,"meta":20,"style":20},"ws.append([6, 60])\nprint(ws.max_row)\n# 2001\n",[22,992,993,1008,1015],{"__ignoreMap":20},[254,994,995,998,1001,1003,1006],{"class":193,"line":256},[254,996,997],{"class":303},"ws.append([",[254,999,1000],{"class":352},"6",[254,1002,434],{"class":303},[254,1004,1005],{"class":352},"60",[254,1007,470],{"class":303},[254,1009,1010,1012],{"class":193,"line":313},[254,1011,353],{"class":352},[254,1013,1014],{"class":303},"(ws.max_row)\n",[254,1016,1017],{"class":193,"line":320},[254,1018,1019],{"class":362},"# 2001\n",[10,1021,1022],{},"Write explicitly to the row after the real data instead:",[14,1024,1026],{"className":290,"code":1025,"language":292,"meta":20,"style":20},"wb = load_workbook(\"ledger.xlsx\")\nws = wb.active\n\ndef append_after_data(ws, values, key_column=1):\n    row = last_data_row(ws, column=key_column) + 1\n    for col, value in enumerate(values, start=1):\n        ws.cell(row=row, column=col, value=value)\n    return row\n\nprint(append_after_data(ws, [6, 60]))\n# 7\nwb.save(\"ledger_appended.xlsx\")\n",[22,1027,1028,1040,1048,1052,1068,1090,1114,1139,1145,1149,1165,1170],{"__ignoreMap":20},[254,1029,1030,1032,1034,1036,1038],{"class":193,"line":256},[254,1031,323],{"class":303},[254,1033,326],{"class":299},[254,1035,329],{"class":303},[254,1037,332],{"class":263},[254,1039,335],{"class":303},[254,1041,1042,1044,1046],{"class":193,"line":313},[254,1043,341],{"class":303},[254,1045,326],{"class":299},[254,1047,346],{"class":303},[254,1049,1050],{"class":193,"line":320},[254,1051,317],{"emptyLinePlaceholder":316},[254,1053,1054,1056,1059,1062,1064,1066],{"class":193,"line":338},[254,1055,646],{"class":299},[254,1057,1058],{"class":649}," append_after_data",[254,1060,1061],{"class":303},"(ws, values, key_column",[254,1063,326],{"class":299},[254,1065,109],{"class":352},[254,1067,587],{"class":303},[254,1069,1070,1073,1075,1078,1080,1082,1085,1087],{"class":193,"line":349},[254,1071,1072],{"class":303},"    row ",[254,1074,326],{"class":299},[254,1076,1077],{"class":303}," last_data_row(ws, ",[254,1079,604],{"class":595},[254,1081,326],{"class":299},[254,1083,1084],{"class":303},"key_column) ",[254,1086,402],{"class":299},[254,1088,1089],{"class":352}," 1\n",[254,1091,1092,1094,1097,1099,1102,1105,1108,1110,1112],{"class":193,"line":359},[254,1093,669],{"class":299},[254,1095,1096],{"class":303}," col, value ",[254,1098,388],{"class":299},[254,1100,1101],{"class":352}," enumerate",[254,1103,1104],{"class":303},"(values, ",[254,1106,1107],{"class":595},"start",[254,1109,326],{"class":299},[254,1111,109],{"class":352},[254,1113,587],{"class":303},[254,1115,1116,1119,1121,1123,1125,1127,1129,1132,1134,1136],{"class":193,"line":366},[254,1117,1118],{"class":303},"        ws.cell(",[254,1120,596],{"class":595},[254,1122,326],{"class":299},[254,1124,706],{"class":303},[254,1126,604],{"class":595},[254,1128,326],{"class":299},[254,1130,1131],{"class":303},"col, ",[254,1133,942],{"class":595},[254,1135,326],{"class":299},[254,1137,1138],{"class":303},"value)\n",[254,1140,1141,1143],{"class":193,"line":371},[254,1142,781],{"class":299},[254,1144,776],{"class":303},[254,1146,1147],{"class":193,"line":410},[254,1148,317],{"emptyLinePlaceholder":316},[254,1150,1151,1153,1156,1158,1160,1162],{"class":193,"line":452},[254,1152,353],{"class":352},[254,1154,1155],{"class":303},"(append_after_data(ws, [",[254,1157,1000],{"class":352},[254,1159,434],{"class":303},[254,1161,1005],{"class":352},[254,1163,1164],{"class":303},"]))\n",[254,1166,1167],{"class":193,"line":473},[254,1168,1169],{"class":362},"# 7\n",[254,1171,1173,1176,1179],{"class":193,"line":1172},12,[254,1174,1175],{"class":303},"wb.save(",[254,1177,1178],{"class":263},"\"ledger_appended.xlsx\"",[254,1180,335],{"class":303},[10,1182,1183,1184,1187,1188,1192],{},"If the sheet will be appended to repeatedly, fix it once (Step 4) so ",[22,1185,1186],{},"append()"," works normally again and every other tool — Excel's Ctrl+End, pandas, a colleague's macro — agrees on where the data ends. ",[36,1189,1191],{"href":1190},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fopenpyxl-append-data-to-existing-excel-sheet\u002F","openpyxl: Append Data to an Existing Excel Sheet"," covers appending in general.",[242,1194,1196],{"id":1195},"step-4-trim-the-empty-rows-for-good","Step 4 — Trim the empty rows for good",[10,1198,1199,1200,1202],{},"Deleting the empty rows below the data removes their cell objects, formatting included, and ",[22,1201,33],{}," drops back to the truth:",[14,1204,1206],{"className":290,"code":1205,"language":292,"meta":20,"style":20},"wb = load_workbook(\"ledger.xlsx\")\nws = wb.active\nlast = last_data_row(ws)\nif ws.max_row > last:\n    ws.delete_rows(last + 1, ws.max_row - last)\nprint(ws.max_row)\n# 6\nwb.save(\"ledger_trimmed.xlsx\")\nprint(load_workbook(\"ledger_trimmed.xlsx\").active.max_row)\n# 6\n",[22,1207,1208,1220,1228,1238,1251,1267,1273,1278,1287,1299],{"__ignoreMap":20},[254,1209,1210,1212,1214,1216,1218],{"class":193,"line":256},[254,1211,323],{"class":303},[254,1213,326],{"class":299},[254,1215,329],{"class":303},[254,1217,332],{"class":263},[254,1219,335],{"class":303},[254,1221,1222,1224,1226],{"class":193,"line":313},[254,1223,341],{"class":303},[254,1225,326],{"class":299},[254,1227,346],{"class":303},[254,1229,1230,1233,1235],{"class":193,"line":320},[254,1231,1232],{"class":303},"last ",[254,1234,326],{"class":299},[254,1236,1237],{"class":303}," last_data_row(ws)\n",[254,1239,1240,1242,1245,1248],{"class":193,"line":338},[254,1241,716],{"class":299},[254,1243,1244],{"class":303}," ws.max_row ",[254,1246,1247],{"class":299},">",[254,1249,1250],{"class":303}," last:\n",[254,1252,1253,1256,1258,1260,1262,1264],{"class":193,"line":349},[254,1254,1255],{"class":303},"    ws.delete_rows(last ",[254,1257,402],{"class":299},[254,1259,405],{"class":352},[254,1261,399],{"class":303},[254,1263,465],{"class":299},[254,1265,1266],{"class":303}," last)\n",[254,1268,1269,1271],{"class":193,"line":359},[254,1270,353],{"class":352},[254,1272,1014],{"class":303},[254,1274,1275],{"class":193,"line":366},[254,1276,1277],{"class":362},"# 6\n",[254,1279,1280,1282,1285],{"class":193,"line":371},[254,1281,1175],{"class":303},[254,1283,1284],{"class":263},"\"ledger_trimmed.xlsx\"",[254,1286,335],{"class":303},[254,1288,1289,1291,1294,1296],{"class":193,"line":410},[254,1290,353],{"class":352},[254,1292,1293],{"class":303},"(load_workbook(",[254,1295,1284],{"class":263},[254,1297,1298],{"class":303},").active.max_row)\n",[254,1300,1301],{"class":193,"line":452},[254,1302,1277],{"class":362},[10,1304,1305,1306,1309,1310,1313,1314,1317,1318,1321],{},"Two cautions. Deleting rows removes their formatting, so if the bold style down to row 1,000 was intentional — a template waiting for data — trimming undoes the template. And ",[22,1307,1308],{},"delete_rows"," does not move merged ranges, conditional formatting ranges or defined names; on a sheet that has any of those ",[511,1311,1312],{},"below"," the data, check them afterwards. The same used-range problem affects columns (",[22,1315,1316],{},"max_column","), and ",[22,1319,1320],{},"delete_cols"," trims those.",[242,1323,1325],{"id":1324},"step-5-read-only-mode-trusts-the-files-own-dimension-tag","Step 5 — Read-only mode trusts the file's own dimension tag",[10,1327,1328,1329,1332],{},"In read-only mode openpyxl does not load cells up front, so it cannot count them. It reports the ",[22,1330,1331],{},"\u003Cdimension ref=\"…\">"," tag that the saving program wrote into the sheet XML — and programs are free to write a generous one:",[14,1334,1336],{"className":290,"code":1335,"language":292,"meta":20,"style":20},"ro = load_workbook(\"ledger.xlsx\", read_only=True)\nrs = ro.active\nprint(rs.max_row, rs.calculate_dimension())\n# 2000 A1:B2000\nro.close()\n",[22,1337,1338,1361,1371,1378,1382],{"__ignoreMap":20},[254,1339,1340,1343,1345,1347,1349,1351,1354,1356,1359],{"class":193,"line":256},[254,1341,1342],{"class":303},"ro ",[254,1344,326],{"class":299},[254,1346,329],{"class":303},[254,1348,332],{"class":263},[254,1350,434],{"class":303},[254,1352,1353],{"class":595},"read_only",[254,1355,326],{"class":299},[254,1357,1358],{"class":352},"True",[254,1360,335],{"class":303},[254,1362,1363,1366,1368],{"class":193,"line":313},[254,1364,1365],{"class":303},"rs ",[254,1367,326],{"class":299},[254,1369,1370],{"class":303}," ro.active\n",[254,1372,1373,1375],{"class":193,"line":320},[254,1374,353],{"class":352},[254,1376,1377],{"class":303},"(rs.max_row, rs.calculate_dimension())\n",[254,1379,1380],{"class":193,"line":338},[254,1381,363],{"class":362},[254,1383,1384],{"class":193,"line":349},[254,1385,1386],{"class":303},"ro.close()\n",[10,1388,1389,1390,1393],{},"To measure rather than trust, call ",[22,1391,1392],{},"reset_dimensions()"," and iterate. Rows then come back as the file actually stores them, and you can stop counting at the last one with data:",[14,1395,1397],{"className":290,"code":1396,"language":292,"meta":20,"style":20},"ro = load_workbook(\"ledger.xlsx\", read_only=True)\nrs = ro.active\nrs.reset_dimensions()\nlast = 0\nfor n, row in enumerate(rs.iter_rows(values_only=True), start=1):\n    if any(v is not None and str(v).strip() != \"\" for v in row):\n        last = n\nprint(last)\n# 6\nro.close()\n",[22,1398,1399,1419,1427,1432,1440,1472,1509,1519,1526,1530],{"__ignoreMap":20},[254,1400,1401,1403,1405,1407,1409,1411,1413,1415,1417],{"class":193,"line":256},[254,1402,1342],{"class":303},[254,1404,326],{"class":299},[254,1406,329],{"class":303},[254,1408,332],{"class":263},[254,1410,434],{"class":303},[254,1412,1353],{"class":595},[254,1414,326],{"class":299},[254,1416,1358],{"class":352},[254,1418,335],{"class":303},[254,1420,1421,1423,1425],{"class":193,"line":313},[254,1422,1365],{"class":303},[254,1424,326],{"class":299},[254,1426,1370],{"class":303},[254,1428,1429],{"class":193,"line":320},[254,1430,1431],{"class":303},"rs.reset_dimensions()\n",[254,1433,1434,1436,1438],{"class":193,"line":338},[254,1435,1232],{"class":303},[254,1437,326],{"class":299},[254,1439,784],{"class":352},[254,1441,1442,1444,1447,1449,1451,1454,1457,1459,1461,1464,1466,1468,1470],{"class":193,"line":349},[254,1443,382],{"class":299},[254,1445,1446],{"class":303}," n, row ",[254,1448,388],{"class":299},[254,1450,1101],{"class":352},[254,1452,1453],{"class":303},"(rs.iter_rows(",[254,1455,1456],{"class":595},"values_only",[254,1458,326],{"class":299},[254,1460,1358],{"class":352},[254,1462,1463],{"class":303},"), ",[254,1465,1107],{"class":595},[254,1467,326],{"class":299},[254,1469,109],{"class":352},[254,1471,587],{"class":303},[254,1473,1474,1477,1479,1482,1484,1486,1488,1490,1492,1495,1497,1499,1501,1504,1506],{"class":193,"line":359},[254,1475,1476],{"class":299},"    if",[254,1478,416],{"class":352},[254,1480,1481],{"class":303},"(v ",[254,1483,737],{"class":299},[254,1485,740],{"class":299},[254,1487,743],{"class":352},[254,1489,746],{"class":299},[254,1491,749],{"class":352},[254,1493,1494],{"class":303},"(v).strip() ",[254,1496,755],{"class":299},[254,1498,758],{"class":263},[254,1500,761],{"class":299},[254,1502,1503],{"class":303}," v ",[254,1505,388],{"class":299},[254,1507,1508],{"class":303}," row):\n",[254,1510,1511,1514,1516],{"class":193,"line":366},[254,1512,1513],{"class":303},"        last ",[254,1515,326],{"class":299},[254,1517,1518],{"class":303}," n\n",[254,1520,1521,1523],{"class":193,"line":371},[254,1522,353],{"class":352},[254,1524,1525],{"class":303},"(last)\n",[254,1527,1528],{"class":193,"line":410},[254,1529,1277],{"class":362},[254,1531,1532],{"class":193,"line":452},[254,1533,1386],{"class":303},[10,1535,1536,1537,1539],{},"This pattern also protects you from the opposite problem: some writers omit the dimension tag or record it too small, and without ",[22,1538,1392],{}," read-only iteration can stop early.",[42,1541,51,1546,51,1549,51,1552,51,1555,51,1560,51,1567,51,1572,51,1576,51,1580,51,1584,51,1588,51,1592,51,1595,51,1601,51,1606,51,1610,51,1613,51,1616,51,1619],{"viewBox":1542,"role":45,"ariaLabelledBy":1543,"xmlns":49,"style":829},"4 0 752 233",[1544,1545],"mr-modes-t","mr-modes-d",[53,1547,1548],{"id":1544},"max_row in normal and read-only mode",[57,1550,1551],{"id":1545},"Two panels. In normal mode max_row is the highest row with a cell object, inflated by formatting, empty strings and cells created by reading. In read-only mode max_row comes from the dimension tag saved in the file and may be stale; reset_dimensions and iterating gives the real extent.",[61,1553],{"x":165,"y":64,"width":838,"height":1554,"fill":67},"233",[61,1556],{"x":842,"y":843,"width":1557,"height":1558,"rx":1559,"fill":956,"stroke":198,"style":199},"351.0","201.0","14",[212,1561,1563],{"transform":1562},"translate(182.5,29.0) scale(1.083)",[216,1564],{"d":1565,"fill":219,"stroke":963,"style":1566},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[19,1568,1571],{"x":1569,"y":1570,"style":72},"195.5","78.0","load_workbook(path)",[193,1573],{"x1":1574,"y1":108,"x2":1575,"y2":108,"stroke":198,"style":82},"36.0","355.0",[19,1577,1579],{"x":1569,"y":1578,"style":132},"111.0","highest row with a cell object",[19,1581,1583],{"x":1569,"y":1582,"style":132},"137.0","formatting and \"\" count",[19,1585,1587],{"x":1569,"y":1586,"style":132},"163.0","reading a cell creates it",[19,1589,1591],{"x":1569,"y":1590,"style":132},"189.0","fix: walk up, or delete_rows",[61,1593],{"x":1594,"y":843,"width":1557,"height":1558,"rx":1559,"fill":147,"stroke":148,"style":199},"389.0",[212,1596,1598],{"transform":1597},"translate(551.5,29.0) scale(1.083)",[216,1599],{"d":1600,"fill":219,"stroke":934,"style":1566},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z",[19,1602,1605],{"x":1603,"y":1570,"style":1604},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","load_workbook(path, read_only=True)",[193,1607],{"x1":1608,"y1":108,"x2":1609,"y2":108,"stroke":148,"style":82},"405.0","724.0",[19,1611,1612],{"x":1603,"y":1578,"style":132},"copied from the dimension tag",[19,1614,1615],{"x":1603,"y":1582,"style":132},"can be too large or too small",[19,1617,1618],{"x":1603,"y":1586,"style":132},"reading creates nothing",[19,1620,1621],{"x":1603,"y":1590,"style":132},"fix: reset_dimensions, then iterate",[242,1623,1625],{"id":1624},"what-pandas-does-with-the-same-file","What pandas does with the same file",[10,1627,1628],{},"pandas drops trailing rows that are entirely empty, so the same workbook reads cleanly:",[14,1630,1632],{"className":290,"code":1631,"language":292,"meta":20,"style":20},"import pandas as pd\n\nprint(pd.read_excel(\"ledger.xlsx\").shape)\n# (5, 2)\n",[22,1633,1634,1647,1651,1663],{"__ignoreMap":20},[254,1635,1636,1638,1641,1644],{"class":193,"line":256},[254,1637,307],{"class":299},[254,1639,1640],{"class":303}," pandas ",[254,1642,1643],{"class":299},"as",[254,1645,1646],{"class":303}," pd\n",[254,1648,1649],{"class":193,"line":313},[254,1650,317],{"emptyLinePlaceholder":316},[254,1652,1653,1655,1658,1660],{"class":193,"line":320},[254,1654,353],{"class":352},[254,1656,1657],{"class":303},"(pd.read_excel(",[254,1659,332],{"class":263},[254,1661,1662],{"class":303},").shape)\n",[254,1664,1665],{"class":193,"line":338},[254,1666,1667],{"class":362},"# (5, 2)\n",[10,1669,1670,1671,1674,1675,1678],{},"That is why the problem often goes unnoticed until someone writes back with openpyxl. It also means a DataFrame's length is a reasonable cross-check: if ",[22,1672,1673],{},"len(df) + 1"," (for the header) is far below ",[22,1676,1677],{},"ws.max_row",", the sheet has an inflated used range worth trimming.",[242,1680,1682],{"id":1681},"common-pitfalls","Common pitfalls",[1684,1685,1686,1696,1712,1720],"ul",{},[484,1687,1688,1695],{},[487,1689,1690,1691,1694],{},"Looping ",[22,1692,1693],{},"for r in range(1, ws.max_row + 1)"," to find blanks."," It is correct but slow on inflated sheets, and in normal mode it creates cells for every coordinate it reads. Walk up from the bottom instead.",[484,1697,1698,1704,1705,1708,1709,1711],{},[487,1699,1700,1701,1703],{},"Using ",[22,1702,33],{}," as a row count."," It is a row ",[511,1706,1707],{},"number",". A table starting at row 5 with 10 rows has ",[22,1710,33],{}," 14.",[484,1713,1714,1719],{},[487,1715,1716,1717,494],{},"Checking only ",[22,1718,431],{}," Empty strings and spaces are values; decide explicitly whether they count.",[484,1721,1722,1725],{},[487,1723,1724],{},"Trimming a template."," Formatted empty rows in a template are there on purpose. Write after the data instead of deleting them.",[242,1727,1729],{"id":1728},"performance-notes","Performance notes",[10,1731,1732,1733,1735,1736,1740],{},"Walking upward costs one row check per empty row below the data, which is trivial for thousands and noticeable only for sheets formatted to the bottom of the grid (1,048,576 rows). For those, check a single key column rather than whole rows, or use read-only mode with ",[22,1734,1392],{},", which streams the XML and never builds the empty rows as objects. ",[36,1737,1739],{"href":1738},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fspeed-up-openpyxl-with-read-only-mode\u002F","Speed Up openpyxl with read_only Mode"," covers streaming in detail.",[242,1742,1744],{"id":1743},"conclusion","Conclusion",[10,1746,1747,1749,1750,1752,1753,1755,1756,1758],{},[22,1748,33],{}," counts cell objects, and formatting, empty strings and your own reads all create them. Find the real end by walking upward to the first row with a value, append explicitly to the row after it, and trim the empty rows with ",[22,1751,1308],{}," when the sheet is yours to clean. In read-only mode, remember that ",[22,1754,33],{}," is whatever the file claims; ",[22,1757,1392],{}," and iteration tell you what is really there.",[242,1760,1762],{"id":1761},"frequently-asked-questions","Frequently asked questions",[10,1764,1765,1768],{},[487,1766,1767],{},"Why is max_row larger than the number of rows with data?","\nmax_row is the highest row number that has a cell object, not the highest row with a value. Formatting, an empty string, or merely reading a cell in normal mode all create cell objects and push max_row down.",[10,1770,1771,1774],{},[487,1772,1773],{},"Why does ws.append() write thousands of rows below my data?","\nappend() always writes to the row after max_row. If formatting or stray cells inflated max_row, the new row lands below them. Find the real last row and write with ws.cell(row=last + 1, ...) or trim the empty rows first.",[10,1776,1777,1780],{},[487,1778,1779],{},"Does pandas read those empty rows?","\nNo. read_excel drops trailing rows that are completely empty, so a DataFrame usually looks right even when openpyxl reports a huge max_row. The problem appears when you write back with openpyxl.",[10,1782,1783,1786],{},[487,1784,1785],{},"Why is max_row wrong in read-only mode but right in normal mode?","\nIn read-only mode openpyxl trusts the dimension tag stored in the file, which the program that saved it may have written generously. Call ws.reset_dimensions() and iterate, or open the file normally, to measure what is really there.",[242,1788,1790],{"id":1789},"related","Related",[1684,1792,1793,1799,1806,1813],{},[484,1794,1795,1796,1798],{},"Up one level: ",[36,1797,39],{"href":38}," — other surprises in reading and writing.",[484,1800,1801,1805],{},[36,1802,1804],{"href":1803},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl\u002F","Find the Last Row with Data in openpyxl"," — the general technique across columns and tables.",[484,1807,1808,1812],{},[36,1809,1811],{"href":1810},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-unnamed-0-columns-when-reading-excel\u002F","Fix \"Unnamed: 0\" Columns When Reading Excel with pandas"," — the same used-range effect, sideways.",[484,1814,1815,1819],{},[36,1816,1818],{"href":1817},"\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"," — loops that do not create cells.",[1821,1822,1823],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":20,"searchDepth":313,"depth":313,"links":1825},[1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837],{"id":244,"depth":313,"text":245},{"id":280,"depth":313,"text":281},{"id":629,"depth":313,"text":630},{"id":977,"depth":313,"text":978},{"id":1195,"depth":313,"text":1196},{"id":1324,"depth":313,"text":1325},{"id":1624,"depth":313,"text":1625},{"id":1681,"depth":313,"text":1682},{"id":1728,"depth":313,"text":1729},{"id":1743,"depth":313,"text":1744},{"id":1761,"depth":313,"text":1762},{"id":1789,"depth":313,"text":1790},"2026-09-25","ws.max_row reports 2,000 on a sheet with six rows of data, and ws.append() writes far below the table. Learn the three things that inflate max_row — formatted cells, cells created by reading, and stale dimension tags — and how to find and fix the real last row.","md",[1842,1844,1846,1848],{"q":1767,"a":1843},"max_row is the highest row number that has a cell object, not the highest row with a value. Formatting, an empty string, or merely reading a cell in normal mode all create cell objects and push max_row down.",{"q":1773,"a":1845},"append() always writes to the row after max_row. If formatting or stray cells inflated max_row, the new row lands below them. Find the real last row and write with ws.cell(row=last + 1, ...) or trim the empty rows first.",{"q":1779,"a":1847},"No. read_excel drops trailing rows that are completely empty, so a DataFrame usually looks right even when openpyxl reports a huge max_row. The problem appears when you write back with openpyxl.",{"q":1785,"a":1849},"In read-only mode openpyxl trusts the dimension tag stored in the file, which the program that saved it may have written generously. Call ws.reset_dimensions() and iterate, or open the file normally, to measure what is really there.","fix openpyxl max_row counting empty rows",null,{"breadcrumb":1853},[1854,1857,1860],{"name":1855,"item":1856},"Home","\u002F",{"name":1858,"item":1859},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":39,"item":38},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows",{"title":1863,"description":1864},"openpyxl max_row Too Large? Fix Empty Rows and append() Gaps","Why openpyxl max_row counts empty formatted rows, why append() then writes thousands of rows down, and how to find the true last row, trim the sheet and handle read-only mode.","fix-openpyxl-max-row-counting-empty-rows","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-max-row-counting-empty-rows\u002Findex",[1868,1869,1870],"troubleshooting","openpyxl","large-files","how-to","KWiO552gghit4SyOUAQjwLoMaXG0KtfdifI4UYzqZHA",[1874,1878],{"title":1875,"path":1876,"stem":1877,"children":-1},"Fix \"openpyxl does not support the old .xls file format\"","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-does-not-support-the-old-xls-format\u002Findex",{"title":1879,"path":1880,"stem":1881,"children":-1},"Fix openpyxl \"TypeError: expected \u003Cclass ...>\"","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-typeerror-expected-class","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-openpyxl-typeerror-expected-class\u002Findex",[1883,1885],{"path":1884,"title":1858},"\u002Fgetting-started-with-python-excel-automation",{"path":1886,"title":39},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[1889,1893,1896,1899,1902,1905],{"path":1890,"title":1891,"kind":1892},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python","Handle Permission Denied When Writing Excel in Python","fix",{"path":1894,"title":1895,"kind":1871},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fwhat-openpyxl-loses-when-saving-an-existing-workbook","What openpyxl Loses When You Save an Existing Workbook",{"path":1897,"title":1898,"kind":1892},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","Fix Excel Serial Numbers Showing Instead of Dates",{"path":1900,"title":1901,"kind":1871},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-daylight-saving-time-in-excel-timestamps","Handle Daylight Saving Time in Excel Timestamps",{"path":1903,"title":1904,"kind":1871},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":1906,"title":1907,"kind":1908},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972067273]