[{"data":1,"prerenderedAt":1873},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl":1839,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl":1847,"children:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl":1852,"related:\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl":1853},{"id":4,"title":5,"body":6,"dateModified":1804,"datePublished":1804,"description":1805,"extension":1806,"faq":1807,"keyword":1818,"kind":1819,"meta":1820,"navigation":268,"path":1828,"seo":1829,"slug":1832,"stem":1833,"topics":1834,"type":1837,"__hash__":1838},"docs\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl\u002Findex.md","Find the Last Row with Data in openpyxl",{"type":7,"value":8,"toc":1790},"minimark",[9,28,187,192,220,227,423,427,440,541,552,566,570,581,739,757,849,853,860,1035,1043,1047,1057,1267,1278,1345,1349,1372,1453,1467,1471,1474,1542,1553,1557,1675,1679,1688,1692,1709,1713,1720,1726,1732,1738,1744,1748,1786],[10,11,12,13,17,18,23,24,27],"p",{},"Almost every script that adds to an existing sheet starts by asking where the data ends, and almost every\none first tries ",[14,15,16],"code",{},"ws.max_row",". On a sheet that someone has formatted down to row 1,000 in advance, that\nanswer is 1,000, and the new rows land far below the table. In read-only mode it can be even further off,\nbecause the value comes from a size recorded in the file rather than from the cells. This guide, part of\n",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002F","Using openpyxl for Excel File Manipulation",",\nexplains what ",[14,25,26],{},"max_row"," really measures and how to find the row you actually want.",[29,30,38,39,38,43,38,47,38,54,38,62,38,66,38,73,38,77,38,81,38,84,38,88,38,90,38,94,38,96,38,98,38,101,38,105,38,109,38,113,38,115,38,117,38,124,38,129,38,132,38,136,38,138,38,140,38,143,38,147,38,149,38,151,38,154,38,157,38,159,38,161,38,164,38,167,38,171,38,173,38,176,38,181],"svg",{"viewBox":31,"role":32,"ariaLabelledBy":33,"xmlns":36,"style":37},"0 0 446 284","img",[34,35],"lr-sheet-t","lr-sheet-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:446px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[40,41,42],"title",{"id":34},"A sheet where max_row and the last data row differ",[44,45,46],"desc",{"id":35},"Data occupies rows 1 to 5. Row 20 holds only a fill colour applied in advance. openpyxl reports max_row as 20, while the last row containing a value is 5.",[48,49],"rect",{"x":50,"y":50,"width":51,"height":52,"fill":53},"0","446","284","#ffffff",[48,55],{"x":56,"y":56,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"20","36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[48,63],{"x":64,"y":56,"width":65,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"56","90",[67,68,72],"text",{"x":69,"y":70,"style":71},"101","40","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[48,74],{"x":75,"y":56,"width":76,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"146","70",[67,78,80],{"x":79,"y":70,"style":71},"181","B",[48,82],{"x":56,"y":83,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"50",[67,85,87],{"x":86,"y":76,"style":71},"38","4",[48,89],{"x":64,"y":83,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[67,91,93],{"x":69,"y":76,"style":92},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","row4",[48,95],{"x":75,"y":83,"width":76,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[67,97,87],{"x":79,"y":76,"style":92},[48,99],{"x":56,"y":100,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"80",[67,102,104],{"x":86,"y":103,"style":71},"100","5",[48,106],{"x":64,"y":100,"width":65,"height":58,"rx":50,"fill":107,"stroke":108,"style":61},"#d9f4f1","var(--teal,#0f9488)",[67,110,112],{"x":69,"y":103,"style":111},"font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle","row5",[48,114],{"x":75,"y":100,"width":76,"height":58,"rx":50,"fill":107,"stroke":108,"style":61},[67,116,104],{"x":79,"y":103,"style":111},[48,118],{"x":119,"y":120,"width":121,"height":122,"rx":123,"fill":107,"stroke":108,"style":61},"236","82","190","26","8",[67,125,128],{"x":126,"y":127,"style":111},"331","99","last data row: 5",[48,130],{"x":56,"y":131,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"110",[67,133,135],{"x":86,"y":134,"style":71},"130","6",[48,137],{"x":64,"y":131,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,139],{"x":75,"y":131,"width":76,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,141],{"x":56,"y":142,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"140",[67,144,146],{"x":86,"y":145,"style":71},"160","…",[48,148],{"x":64,"y":142,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,150],{"x":75,"y":142,"width":76,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,152],{"x":56,"y":153,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"170",[67,155,156],{"x":86,"y":121,"style":71},"19",[48,158],{"x":64,"y":153,"width":65,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,160],{"x":75,"y":153,"width":76,"height":58,"rx":50,"fill":53,"stroke":60,"style":61},[48,162],{"x":56,"y":163,"width":57,"height":58,"rx":50,"fill":59,"stroke":60,"style":61},"200",[67,165,56],{"x":86,"y":166,"style":71},"220",[48,168],{"x":64,"y":163,"width":65,"height":58,"rx":50,"fill":169,"stroke":170,"style":61},"#fdefd8","var(--gold,#b4740a)",[48,172],{"x":75,"y":163,"width":76,"height":58,"rx":50,"fill":169,"stroke":170,"style":61},[48,174],{"x":119,"y":175,"width":121,"height":122,"rx":123,"fill":169,"stroke":170,"style":61},"202",[67,177,180],{"x":126,"y":178,"style":179},"219","font-size:11.5px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","formatted only: max_row",[67,182,186],{"x":183,"y":184,"style":185},"223","262","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","row 20 counts as used because it has a style, not a value",[188,189,191],"h2",{"id":190},"prerequisites","Prerequisites",[193,194,199],"pre",{"className":195,"code":196,"language":197,"meta":198,"style":198},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[14,200,201],{"__ignoreMap":198},[202,203,206,210,214,217],"span",{"class":204,"line":205},"line",1,[202,207,209],{"class":208},"sMTad","pip",[202,211,213],{"class":212},"srMev"," install",[202,215,216],{"class":212}," openpyxl",[202,218,219],{"class":212}," pandas\n",[10,221,222,223,226],{},"This workbook has five rows of data, a formatted but empty cell in row 20, and an empty string typed into ",[14,224,225],{},"B10"," — the three things that mislead naive checks:",[193,228,232],{"className":229,"code":230,"language":231,"meta":198,"style":198},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.styles import PatternFill\n\nwb = Workbook()\nws = wb.active\nws.title = \"Log\"\nfor number in range(1, 6):\n    ws.append([f\"row{number}\", number])\nws[\"A20\"].fill = PatternFill(\"solid\", start_color=\"FFEB9C\")\nws[\"B10\"] = \"\"\nwb.save(\"log.xlsx\")\n","python",[14,233,234,250,263,270,282,293,304,334,362,396,412],{"__ignoreMap":198},[202,235,236,240,244,247],{"class":204,"line":205},[202,237,239],{"class":238},"s-kum","from",[202,241,243],{"class":242},"skGVy"," openpyxl ",[202,245,246],{"class":238},"import",[202,248,249],{"class":242}," Workbook\n",[202,251,253,255,258,260],{"class":204,"line":252},2,[202,254,239],{"class":238},[202,256,257],{"class":242}," openpyxl.styles ",[202,259,246],{"class":238},[202,261,262],{"class":242}," PatternFill\n",[202,264,266],{"class":204,"line":265},3,[202,267,269],{"emptyLinePlaceholder":268},true,"\n",[202,271,273,276,279],{"class":204,"line":272},4,[202,274,275],{"class":242},"wb ",[202,277,278],{"class":238},"=",[202,280,281],{"class":242}," Workbook()\n",[202,283,285,288,290],{"class":204,"line":284},5,[202,286,287],{"class":242},"ws ",[202,289,278],{"class":238},[202,291,292],{"class":242}," wb.active\n",[202,294,296,299,301],{"class":204,"line":295},6,[202,297,298],{"class":242},"ws.title ",[202,300,278],{"class":238},[202,302,303],{"class":212}," \"Log\"\n",[202,305,307,310,313,316,320,323,326,329,331],{"class":204,"line":306},7,[202,308,309],{"class":238},"for",[202,311,312],{"class":242}," number ",[202,314,315],{"class":238},"in",[202,317,319],{"class":318},"sP0c6"," range",[202,321,322],{"class":242},"(",[202,324,325],{"class":318},"1",[202,327,328],{"class":242},", ",[202,330,135],{"class":318},[202,332,333],{"class":242},"):\n",[202,335,337,340,343,346,350,353,356,359],{"class":204,"line":336},8,[202,338,339],{"class":242},"    ws.append([",[202,341,342],{"class":238},"f",[202,344,345],{"class":212},"\"row",[202,347,349],{"class":348},"sSjpA","{",[202,351,352],{"class":242},"number",[202,354,355],{"class":348},"}",[202,357,358],{"class":212},"\"",[202,360,361],{"class":242},", number])\n",[202,363,365,368,371,374,376,379,382,384,388,390,393],{"class":204,"line":364},9,[202,366,367],{"class":242},"ws[",[202,369,370],{"class":212},"\"A20\"",[202,372,373],{"class":242},"].fill ",[202,375,278],{"class":238},[202,377,378],{"class":242}," PatternFill(",[202,380,381],{"class":212},"\"solid\"",[202,383,328],{"class":242},[202,385,387],{"class":386},"sa561","start_color",[202,389,278],{"class":238},[202,391,392],{"class":212},"\"FFEB9C\"",[202,394,395],{"class":242},")\n",[202,397,399,401,404,407,409],{"class":204,"line":398},10,[202,400,367],{"class":242},[202,402,403],{"class":212},"\"B10\"",[202,405,406],{"class":242},"] ",[202,408,278],{"class":238},[202,410,411],{"class":212}," \"\"\n",[202,413,415,418,421],{"class":204,"line":414},11,[202,416,417],{"class":242},"wb.save(",[202,419,420],{"class":212},"\"log.xlsx\"",[202,422,395],{"class":242},[188,424,426],{"id":425},"step-1-see-what-max_row-actually-reports","Step 1 — See what max_row actually reports",[10,428,429,431,432,436,437,439],{},[14,430,26],{}," is the highest row number of any cell openpyxl holds for the sheet. A cell is held if it has a value, ",[433,434,435],"em",{},"or"," a style, ",[433,438,435],{}," has simply been accessed\nin code. It is a measure of the sheet's extent, not of its data.",[193,441,443],{"className":229,"code":442,"language":231,"meta":198,"style":198},"from openpyxl import load_workbook\n\nwb = load_workbook(\"log.xlsx\")\nws = wb[\"Log\"]\nprint(ws.max_row, ws.dimensions)       # 20 A1:B20\n\nws.cell(row=100, column=1)             # merely reading a cell creates it\nprint(ws.max_row)                      # 100\n",[14,444,445,456,460,473,488,500,504,531],{"__ignoreMap":198},[202,446,447,449,451,453],{"class":204,"line":205},[202,448,239],{"class":238},[202,450,243],{"class":242},[202,452,246],{"class":238},[202,454,455],{"class":242}," load_workbook\n",[202,457,458],{"class":204,"line":252},[202,459,269],{"emptyLinePlaceholder":268},[202,461,462,464,466,469,471],{"class":204,"line":265},[202,463,275],{"class":242},[202,465,278],{"class":238},[202,467,468],{"class":242}," load_workbook(",[202,470,420],{"class":212},[202,472,395],{"class":242},[202,474,475,477,479,482,485],{"class":204,"line":272},[202,476,287],{"class":242},[202,478,278],{"class":238},[202,480,481],{"class":242}," wb[",[202,483,484],{"class":212},"\"Log\"",[202,486,487],{"class":242},"]\n",[202,489,490,493,496],{"class":204,"line":284},[202,491,492],{"class":318},"print",[202,494,495],{"class":242},"(ws.max_row, ws.dimensions)       ",[202,497,499],{"class":498},"s-wDw","# 20 A1:B20\n",[202,501,502],{"class":204,"line":295},[202,503,269],{"emptyLinePlaceholder":268},[202,505,506,509,512,514,516,518,521,523,525,528],{"class":204,"line":306},[202,507,508],{"class":242},"ws.cell(",[202,510,511],{"class":386},"row",[202,513,278],{"class":238},[202,515,103],{"class":318},[202,517,328],{"class":242},[202,519,520],{"class":386},"column",[202,522,278],{"class":238},[202,524,325],{"class":318},[202,526,527],{"class":242},")             ",[202,529,530],{"class":498},"# merely reading a cell creates it\n",[202,532,533,535,538],{"class":204,"line":336},[202,534,492],{"class":318},[202,536,537],{"class":242},"(ws.max_row)                      ",[202,539,540],{"class":498},"# 100\n",[10,542,543,544,547,548,551],{},"Excel has exactly the same notion. Press Ctrl+End on this sheet and Excel jumps to ",[14,545,546],{},"B20",", not ",[14,549,550],{},"B5",", because its \"used range\" also includes\nformatted cells — which is why people who format a template generously are often surprised by how far down a script writes. The difference is\nthat Excel recalculates its used range when the file is saved after cleanup, while openpyxl simply reports the cells that exist in the file and\nin memory at the moment you ask.",[10,553,554,555,558,559,562,563,565],{},"The second print is the less obvious trap. ",[14,556,557],{},"ws.cell(row, column)"," and ",[14,560,561],{},"ws[\"A100\"]"," create an empty cell object if none exists, which extends ",[14,564,26],{},". A loop that\nprobes rows below the data to \"look for the end\" moves the end further away every time it looks.",[188,567,569],{"id":568},"step-2-find-the-last-row-with-a-value-on-the-whole-sheet","Step 2 — Find the last row with a value on the whole sheet",[10,571,572,573,576,577,580],{},"Walk the rows from the top with ",[14,574,575],{},"iter_rows(values_only=True)",", which yields tuples of plain values and never creates cells, and remember the last row that\ncontains anything other than ",[14,578,579],{},"None"," or an empty string.",[193,582,584],{"className":229,"code":583,"language":231,"meta":198,"style":198},"def last_data_row(ws) -> int:\n    last = 0\n    for number, values in enumerate(ws.iter_rows(values_only=True), start=1):\n        if any(value not in (None, \"\") for value in values):\n            last = number\n    return last\n\nwb = load_workbook(\"log.xlsx\")\nprint(last_data_row(wb[\"Log\"]))        # 5\n",[14,585,586,604,614,650,690,700,708,712,724],{"__ignoreMap":198},[202,587,588,591,595,598,601],{"class":204,"line":205},[202,589,590],{"class":238},"def",[202,592,594],{"class":593},"s_Opv"," last_data_row",[202,596,597],{"class":242},"(ws) -> ",[202,599,600],{"class":318},"int",[202,602,603],{"class":242},":\n",[202,605,606,609,611],{"class":204,"line":252},[202,607,608],{"class":242},"    last ",[202,610,278],{"class":238},[202,612,613],{"class":318}," 0\n",[202,615,616,619,622,624,627,630,633,635,638,641,644,646,648],{"class":204,"line":265},[202,617,618],{"class":238},"    for",[202,620,621],{"class":242}," number, values ",[202,623,315],{"class":238},[202,625,626],{"class":318}," enumerate",[202,628,629],{"class":242},"(ws.iter_rows(",[202,631,632],{"class":386},"values_only",[202,634,278],{"class":238},[202,636,637],{"class":318},"True",[202,639,640],{"class":242},"), ",[202,642,643],{"class":386},"start",[202,645,278],{"class":238},[202,647,325],{"class":318},[202,649,333],{"class":242},[202,651,652,655,658,661,664,667,670,672,674,677,680,682,685,687],{"class":204,"line":272},[202,653,654],{"class":238},"        if",[202,656,657],{"class":318}," any",[202,659,660],{"class":242},"(value ",[202,662,663],{"class":238},"not",[202,665,666],{"class":238}," in",[202,668,669],{"class":242}," (",[202,671,579],{"class":318},[202,673,328],{"class":242},[202,675,676],{"class":212},"\"\"",[202,678,679],{"class":242},") ",[202,681,309],{"class":238},[202,683,684],{"class":242}," value ",[202,686,315],{"class":238},[202,688,689],{"class":242}," values):\n",[202,691,692,695,697],{"class":204,"line":284},[202,693,694],{"class":242},"            last ",[202,696,278],{"class":238},[202,698,699],{"class":242}," number\n",[202,701,702,705],{"class":204,"line":295},[202,703,704],{"class":238},"    return",[202,706,707],{"class":242}," last\n",[202,709,710],{"class":204,"line":306},[202,711,269],{"emptyLinePlaceholder":268},[202,713,714,716,718,720,722],{"class":204,"line":336},[202,715,275],{"class":242},[202,717,278],{"class":238},[202,719,468],{"class":242},[202,721,420],{"class":212},[202,723,395],{"class":242},[202,725,726,728,731,733,736],{"class":204,"line":364},[202,727,492],{"class":318},[202,729,730],{"class":242},"(last_data_row(wb[",[202,732,484],{"class":212},[202,734,735],{"class":242},"]))        ",[202,737,738],{"class":498},"# 5\n",[10,740,741,742,744,745,748,749,752,753,756],{},"Treating ",[14,743,676],{}," as empty matters for sheets written by other scripts, which often write empty strings instead of leaving cells blank. If whitespace-only cells should\ncount as empty too, test ",[14,746,747],{},"str(value).strip()"," instead. Scanning from the top rather than the bottom looks wasteful, but ",[14,750,751],{},"iter_rows"," is the cheap access path, and a\nbottom-up scan with ",[14,754,755],{},"ws.cell()"," would create cells as it goes.",[29,758,38,764,38,767,38,770,38,774,38,782,38,787,38,790,38,794,38,797,38,801,38,804,38,809,38,813,38,816,38,819,38,822,38,827,38,832,38,836,38,839,38,842,38,845],{"viewBox":759,"role":32,"ariaLabelledBy":760,"xmlns":36,"style":763},"0 0 760 191",[761,762],"lr-methods-t","lr-methods-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,765,766],{"id":761},"Ways of finding where the data ends",[44,768,769],{"id":762},"max_row reports the extent including formatted and accessed cells. A values_only scan finds the last row containing a value. A key-column scan finds the last row where a required column is filled. pandas drops trailing empty rows when it reads.",[48,771],{"x":50,"y":50,"width":772,"height":773,"fill":53},"760","191",[48,775],{"x":56,"y":56,"width":776,"height":777,"rx":778,"fill":779,"stroke":780,"style":781},"226.7","127","14","#fee8f2","var(--accent,#f43f8f)","stroke-width:2px",[67,783,16],{"x":784,"y":785,"style":786},"133.3","46","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[204,788],{"x1":57,"y1":64,"x2":789,"y2":64,"stroke":780,"style":61},"230.7",[67,791,793],{"x":784,"y":792,"style":92},"78","extent of held cells",[67,795,796],{"x":784,"y":69,"style":92},"counts styles, accesses",[67,798,800],{"x":784,"y":799,"style":92},"124","fast, often wrong",[48,802],{"x":803,"y":56,"width":776,"height":777,"rx":778,"fill":107,"stroke":108,"style":781},"266.7",[67,805,808],{"x":806,"y":785,"style":807},"380","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","values_only scan",[204,810],{"x1":811,"y1":64,"x2":812,"y2":64,"stroke":108,"style":61},"282.7","477.3",[67,814,815],{"x":806,"y":792,"style":92},"last row with any value",[67,817,818],{"x":806,"y":69,"style":92},"never creates cells",[67,820,821],{"x":806,"y":799,"style":92},"right for most sheets",[48,823],{"x":824,"y":56,"width":776,"height":777,"rx":778,"fill":825,"stroke":826,"style":781},"513.3","#f0f4ff","var(--brand,#5b5cf0)",[67,828,831],{"x":829,"y":785,"style":830},"626.7","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","key column scan",[204,833],{"x1":834,"y1":64,"x2":835,"y2":64,"stroke":826,"style":61},"529.3","724",[67,837,838],{"x":829,"y":792,"style":92},"last row with a key",[67,840,841],{"x":829,"y":69,"style":92},"ignores stray notes",[67,843,844],{"x":829,"y":799,"style":92},"right for tables",[67,846,848],{"x":806,"y":847,"style":185},"177","decide what \"data\" means before counting rows",[188,850,852],{"id":851},"step-3-find-the-last-row-in-one-column","Step 3 — Find the last row in one column",[10,854,855,856,859],{},"Sheets often have notes, totals or formulas typed below or beside the table. The row you want is then the last one where a ",[433,857,858],{},"key"," column — an ID, a date — is filled,\nregardless of what else sits on the sheet. Restrict the scan to that column.",[193,861,863],{"className":229,"code":862,"language":231,"meta":198,"style":198},"from openpyxl.utils import column_index_from_string\n\ndef last_row_in_column(ws, column: str) -> int:\n    index = column_index_from_string(column)\n    last = 0\n    for number, (value,) in enumerate(ws.iter_rows(min_col=index, max_col=index, values_only=True), start=1):\n        if value not in (None, \"\"):\n            last = number\n    return last\n\nwb = load_workbook(\"log.xlsx\")\nprint(last_row_in_column(wb[\"Log\"], \"B\"))    # 5 — B10 holds \"\", which does not count\n",[14,864,865,877,881,901,911,919,963,983,991,997,1001,1013],{"__ignoreMap":198},[202,866,867,869,872,874],{"class":204,"line":205},[202,868,239],{"class":238},[202,870,871],{"class":242}," openpyxl.utils ",[202,873,246],{"class":238},[202,875,876],{"class":242}," column_index_from_string\n",[202,878,879],{"class":204,"line":252},[202,880,269],{"emptyLinePlaceholder":268},[202,882,883,885,888,891,894,897,899],{"class":204,"line":265},[202,884,590],{"class":238},[202,886,887],{"class":593}," last_row_in_column",[202,889,890],{"class":242},"(ws, column: ",[202,892,893],{"class":318},"str",[202,895,896],{"class":242},") -> ",[202,898,600],{"class":318},[202,900,603],{"class":242},[202,902,903,906,908],{"class":204,"line":272},[202,904,905],{"class":242},"    index ",[202,907,278],{"class":238},[202,909,910],{"class":242}," column_index_from_string(column)\n",[202,912,913,915,917],{"class":204,"line":284},[202,914,608],{"class":242},[202,916,278],{"class":238},[202,918,613],{"class":318},[202,920,921,923,926,928,930,932,935,937,940,943,945,947,949,951,953,955,957,959,961],{"class":204,"line":295},[202,922,618],{"class":238},[202,924,925],{"class":242}," number, (value,) ",[202,927,315],{"class":238},[202,929,626],{"class":318},[202,931,629],{"class":242},[202,933,934],{"class":386},"min_col",[202,936,278],{"class":238},[202,938,939],{"class":242},"index, ",[202,941,942],{"class":386},"max_col",[202,944,278],{"class":238},[202,946,939],{"class":242},[202,948,632],{"class":386},[202,950,278],{"class":238},[202,952,637],{"class":318},[202,954,640],{"class":242},[202,956,643],{"class":386},[202,958,278],{"class":238},[202,960,325],{"class":318},[202,962,333],{"class":242},[202,964,965,967,969,971,973,975,977,979,981],{"class":204,"line":306},[202,966,654],{"class":238},[202,968,684],{"class":242},[202,970,663],{"class":238},[202,972,666],{"class":238},[202,974,669],{"class":242},[202,976,579],{"class":318},[202,978,328],{"class":242},[202,980,676],{"class":212},[202,982,333],{"class":242},[202,984,985,987,989],{"class":204,"line":336},[202,986,694],{"class":242},[202,988,278],{"class":238},[202,990,699],{"class":242},[202,992,993,995],{"class":204,"line":364},[202,994,704],{"class":238},[202,996,707],{"class":242},[202,998,999],{"class":204,"line":398},[202,1000,269],{"emptyLinePlaceholder":268},[202,1002,1003,1005,1007,1009,1011],{"class":204,"line":414},[202,1004,275],{"class":242},[202,1006,278],{"class":238},[202,1008,468],{"class":242},[202,1010,420],{"class":212},[202,1012,395],{"class":242},[202,1014,1016,1018,1021,1023,1026,1029,1032],{"class":204,"line":1015},12,[202,1017,492],{"class":318},[202,1019,1020],{"class":242},"(last_row_in_column(wb[",[202,1022,484],{"class":212},[202,1024,1025],{"class":242},"], ",[202,1027,1028],{"class":212},"\"B\"",[202,1030,1031],{"class":242},"))    ",[202,1033,1034],{"class":498},"# 5 — B10 holds \"\", which does not count\n",[10,1036,1037,1038,1042],{},"For a table with a header row and occasional blank values in some columns, choose the column that is mandatory for every record. The same idea of anchoring on a\nrequired field appears in ",[19,1039,1041],{"href":1040},"\u002Fadvanced-data-transformation-and-cleaning\u002Fhandling-missing-data-in-excel-reports\u002Fdrop-rows-with-missing-required-fields-in-pandas\u002F","Drop Rows with Missing Required Fields in pandas",".",[188,1044,1046],{"id":1045},"step-4-append-below-the-real-data","Step 4 — Append below the real data",[10,1048,1049,1052,1053,1056],{},[14,1050,1051],{},"ws.append()"," writes to ",[14,1054,1055],{},"max_row + 1",", so on this sheet it would put the next record in row 21, leaving fifteen blank rows between the old data and the new. Write\nexplicitly below the last data row instead.",[193,1058,1060],{"className":229,"code":1059,"language":231,"meta":198,"style":198},"def append_after_data(ws, rows: list[list], key_column: str = \"A\") -> int:\n    next_row = last_row_in_column(ws, key_column) + 1\n    for offset, values in enumerate(rows):\n        for column, value in enumerate(values, start=1):\n            ws.cell(row=next_row + offset, column=column, value=value)\n    return next_row\n\nwb = load_workbook(\"log.xlsx\")\nws = wb[\"Log\"]\nstart = append_after_data(ws, [[\"row6\", 6], [\"row7\", 7]])\nwb.save(\"log_appended.xlsx\")\nprint(\"appended from row\", start)            # 6\n",[14,1061,1062,1092,1108,1122,1145,1177,1184,1188,1200,1212,1243,1252],{"__ignoreMap":198},[202,1063,1064,1066,1069,1072,1075,1078,1080,1083,1086,1088,1090],{"class":204,"line":205},[202,1065,590],{"class":238},[202,1067,1068],{"class":593}," append_after_data",[202,1070,1071],{"class":242},"(ws, rows: list[",[202,1073,1074],{"class":318},"list",[202,1076,1077],{"class":242},"], key_column: ",[202,1079,893],{"class":318},[202,1081,1082],{"class":238}," =",[202,1084,1085],{"class":212}," \"A\"",[202,1087,896],{"class":242},[202,1089,600],{"class":318},[202,1091,603],{"class":242},[202,1093,1094,1097,1099,1102,1105],{"class":204,"line":252},[202,1095,1096],{"class":242},"    next_row ",[202,1098,278],{"class":238},[202,1100,1101],{"class":242}," last_row_in_column(ws, key_column) ",[202,1103,1104],{"class":238},"+",[202,1106,1107],{"class":318}," 1\n",[202,1109,1110,1112,1115,1117,1119],{"class":204,"line":265},[202,1111,618],{"class":238},[202,1113,1114],{"class":242}," offset, values ",[202,1116,315],{"class":238},[202,1118,626],{"class":318},[202,1120,1121],{"class":242},"(rows):\n",[202,1123,1124,1127,1130,1132,1134,1137,1139,1141,1143],{"class":204,"line":272},[202,1125,1126],{"class":238},"        for",[202,1128,1129],{"class":242}," column, value ",[202,1131,315],{"class":238},[202,1133,626],{"class":318},[202,1135,1136],{"class":242},"(values, ",[202,1138,643],{"class":386},[202,1140,278],{"class":238},[202,1142,325],{"class":318},[202,1144,333],{"class":242},[202,1146,1147,1150,1152,1154,1157,1159,1162,1164,1166,1169,1172,1174],{"class":204,"line":284},[202,1148,1149],{"class":242},"            ws.cell(",[202,1151,511],{"class":386},[202,1153,278],{"class":238},[202,1155,1156],{"class":242},"next_row ",[202,1158,1104],{"class":238},[202,1160,1161],{"class":242}," offset, ",[202,1163,520],{"class":386},[202,1165,278],{"class":238},[202,1167,1168],{"class":242},"column, ",[202,1170,1171],{"class":386},"value",[202,1173,278],{"class":238},[202,1175,1176],{"class":242},"value)\n",[202,1178,1179,1181],{"class":204,"line":295},[202,1180,704],{"class":238},[202,1182,1183],{"class":242}," next_row\n",[202,1185,1186],{"class":204,"line":306},[202,1187,269],{"emptyLinePlaceholder":268},[202,1189,1190,1192,1194,1196,1198],{"class":204,"line":336},[202,1191,275],{"class":242},[202,1193,278],{"class":238},[202,1195,468],{"class":242},[202,1197,420],{"class":212},[202,1199,395],{"class":242},[202,1201,1202,1204,1206,1208,1210],{"class":204,"line":364},[202,1203,287],{"class":242},[202,1205,278],{"class":238},[202,1207,481],{"class":242},[202,1209,484],{"class":212},[202,1211,487],{"class":242},[202,1213,1214,1217,1219,1222,1225,1227,1229,1232,1235,1237,1240],{"class":204,"line":398},[202,1215,1216],{"class":242},"start ",[202,1218,278],{"class":238},[202,1220,1221],{"class":242}," append_after_data(ws, [[",[202,1223,1224],{"class":212},"\"row6\"",[202,1226,328],{"class":242},[202,1228,135],{"class":318},[202,1230,1231],{"class":242},"], [",[202,1233,1234],{"class":212},"\"row7\"",[202,1236,328],{"class":242},[202,1238,1239],{"class":318},"7",[202,1241,1242],{"class":242},"]])\n",[202,1244,1245,1247,1250],{"class":204,"line":414},[202,1246,417],{"class":242},[202,1248,1249],{"class":212},"\"log_appended.xlsx\"",[202,1251,395],{"class":242},[202,1253,1254,1256,1258,1261,1264],{"class":204,"line":1015},[202,1255,492],{"class":318},[202,1257,322],{"class":242},[202,1259,1260],{"class":212},"\"appended from row\"",[202,1262,1263],{"class":242},", start)            ",[202,1265,1266],{"class":498},"# 6\n",[10,1268,1269,1270,1273,1274,1042],{},"Writing with ",[14,1271,1272],{},"ws.cell(..., value=...)"," into rows that already have formatting keeps that formatting, which is usually what the person who pre-formatted the sheet\nintended. The wider topic of adding to existing workbooks, including tables that should grow, is covered in\n",[19,1275,1277],{"href":1276},"\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",[29,1279,38,1285,38,1288,38,1291,38,1295,38,1299,38,1302,38,1307,38,1312,38,1318,38,1323,38,1325,38,1329,38,1332,38,1336,38,1340],{"viewBox":1280,"role":32,"ariaLabelledBy":1281,"xmlns":36,"style":1284},"0 0 720 168",[1282,1283],"lr-append-t","lr-append-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[40,1286,1287],{"id":1282},"Where new rows land with each approach",[44,1289,1290],{"id":1283},"ws.append writes after max_row, so new data lands in row 21 on a sheet formatted to row 20. Writing after the last data row places it in row 6, directly under the existing data.",[48,1292],{"x":50,"y":50,"width":1293,"height":1294,"fill":53},"720","168",[67,1296,1298],{"x":56,"y":58,"style":1297},"font-size:12px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","first new row on a sheet with data to row 5 and formatting to row 20",[48,1300],{"x":56,"y":785,"width":153,"height":1301,"rx":135,"fill":53,"stroke":60,"style":61},"28",[67,1303,1051],{"x":1304,"y":1305,"style":1306},"105","65","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:middle",[48,1308],{"x":1309,"y":1310,"width":1311,"height":122,"rx":135,"fill":59,"stroke":60,"style":61},"204","47","330",[48,1313],{"x":1314,"y":1315,"width":1316,"height":1317,"rx":104,"fill":779,"stroke":780,"style":61},"205","48","295","24",[67,1319,1322],{"x":1320,"y":1305,"style":1321},"546","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:start","row 21",[48,1324],{"x":56,"y":65,"width":153,"height":1301,"rx":135,"fill":53,"stroke":60,"style":61},[67,1326,1328],{"x":1304,"y":1327,"style":1306},"109","after last data row",[48,1330],{"x":1309,"y":1331,"width":1311,"height":122,"rx":135,"fill":59,"stroke":60,"style":61},"91",[48,1333],{"x":1314,"y":1334,"width":1335,"height":1317,"rx":104,"fill":107,"stroke":108,"style":61},"92","80.5",[67,1337,1339],{"x":1320,"y":1327,"style":1338},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","row 6",[67,1341,1344],{"x":1342,"y":1343,"style":185},"360","150","append() trusts max_row; explicit writes trust the data",[188,1346,1348],{"id":1347},"step-5-handle-read-only-mode","Step 5 — Handle read-only mode",[10,1350,1351,1352,1355,1356,1358,1359,1362,1363,1366,1367,1369,1370,1042],{},"In ",[14,1353,1354],{},"read_only=True"," mode, openpyxl does not load cells up front, so ",[14,1357,26],{}," comes from the ",[14,1360,1361],{},"\u003Cdimension>"," element saved in the file. Some applications write it\naccurately, some write the extent of formatting, and some do not write it at all. ",[14,1364,1365],{},"reset_dimensions()"," discards it — and then ",[14,1368,26],{}," is ",[14,1371,579],{},[193,1373,1375],{"className":229,"code":1374,"language":231,"meta":198,"style":198},"wb = load_workbook(\"log.xlsx\", read_only=True)\nws = wb[\"Log\"]\nprint(ws.max_row)                          # whatever the file recorded\nws.reset_dimensions()\nprint(ws.max_row)                          # None — unknown until you read\n\nprint(last_data_row(ws))                   # the scan works in read-only mode too\nwb.close()\n",[14,1376,1377,1398,1410,1420,1425,1434,1438,1448],{"__ignoreMap":198},[202,1378,1379,1381,1383,1385,1387,1389,1392,1394,1396],{"class":204,"line":205},[202,1380,275],{"class":242},[202,1382,278],{"class":238},[202,1384,468],{"class":242},[202,1386,420],{"class":212},[202,1388,328],{"class":242},[202,1390,1391],{"class":386},"read_only",[202,1393,278],{"class":238},[202,1395,637],{"class":318},[202,1397,395],{"class":242},[202,1399,1400,1402,1404,1406,1408],{"class":204,"line":252},[202,1401,287],{"class":242},[202,1403,278],{"class":238},[202,1405,481],{"class":242},[202,1407,484],{"class":212},[202,1409,487],{"class":242},[202,1411,1412,1414,1417],{"class":204,"line":265},[202,1413,492],{"class":318},[202,1415,1416],{"class":242},"(ws.max_row)                          ",[202,1418,1419],{"class":498},"# whatever the file recorded\n",[202,1421,1422],{"class":204,"line":272},[202,1423,1424],{"class":242},"ws.reset_dimensions()\n",[202,1426,1427,1429,1431],{"class":204,"line":284},[202,1428,492],{"class":318},[202,1430,1416],{"class":242},[202,1432,1433],{"class":498},"# None — unknown until you read\n",[202,1435,1436],{"class":204,"line":295},[202,1437,269],{"emptyLinePlaceholder":268},[202,1439,1440,1442,1445],{"class":204,"line":306},[202,1441,492],{"class":318},[202,1443,1444],{"class":242},"(last_data_row(ws))                   ",[202,1446,1447],{"class":498},"# the scan works in read-only mode too\n",[202,1449,1450],{"class":204,"line":336},[202,1451,1452],{"class":242},"wb.close()\n",[10,1454,1455,1456,1458,1459,1462,1463,1042],{},"The ",[14,1457,575],{}," scan works identically in read-only mode and streams the sheet without holding it in memory, which makes it the right tool for\nlarge files. Remember to ",[14,1460,1461],{},"close()"," a read-only workbook, because it keeps the file open. Read-only loading in general is covered in\n",[19,1464,1466],{"href":1465},"\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",[188,1468,1470],{"id":1469},"step-6-let-pandas-do-it-when-you-only-need-the-data","Step 6 — Let pandas do it when you only need the data",[10,1472,1473],{},"If the goal is to read the data rather than to write below it, pandas already ignores trailing empty rows and columns. The length of the frame plus the header row is\nthe last data row, provided the table starts in row 1.",[193,1475,1477],{"className":229,"code":1476,"language":231,"meta":198,"style":198},"import pandas as pd\n\nframe = pd.read_excel(\"log.xlsx\", sheet_name=\"Log\", header=None)\nprint(len(frame))                          # 5\n",[14,1478,1479,1492,1496,1528],{"__ignoreMap":198},[202,1480,1481,1483,1486,1489],{"class":204,"line":205},[202,1482,246],{"class":238},[202,1484,1485],{"class":242}," pandas ",[202,1487,1488],{"class":238},"as",[202,1490,1491],{"class":242}," pd\n",[202,1493,1494],{"class":204,"line":252},[202,1495,269],{"emptyLinePlaceholder":268},[202,1497,1498,1501,1503,1506,1508,1510,1513,1515,1517,1519,1522,1524,1526],{"class":204,"line":265},[202,1499,1500],{"class":242},"frame ",[202,1502,278],{"class":238},[202,1504,1505],{"class":242}," pd.read_excel(",[202,1507,420],{"class":212},[202,1509,328],{"class":242},[202,1511,1512],{"class":386},"sheet_name",[202,1514,278],{"class":238},[202,1516,484],{"class":212},[202,1518,328],{"class":242},[202,1520,1521],{"class":386},"header",[202,1523,278],{"class":238},[202,1525,579],{"class":318},[202,1527,395],{"class":242},[202,1529,1530,1532,1534,1537,1540],{"class":204,"line":272},[202,1531,492],{"class":318},[202,1533,322],{"class":242},[202,1535,1536],{"class":318},"len",[202,1538,1539],{"class":242},"(frame))                          ",[202,1541,738],{"class":498},[10,1543,1544,1545,1548,1549,1042],{},"That shortcut breaks down if there are blank rows ",[433,1546,1547],{},"inside"," the data — pandas keeps those as rows of missing values — or if the table starts lower on the sheet. For\nsheets with banners above the data, locate the header first, as in\n",[19,1550,1552],{"href":1551},"\u002Fadvanced-data-transformation-and-cleaning\u002Fextracting-data-from-messy-excel-sheets\u002Ffind-the-header-row-in-an-excel-sheet-with-a-shifting-layout\u002F","Find the Header Row in an Excel Sheet with a Shifting Layout",[188,1554,1556],{"id":1555},"common-pitfalls","Common pitfalls",[1558,1559,1560,1576],"table",{},[1561,1562,1563],"thead",{},[1564,1565,1566,1570,1573],"tr",{},[1567,1568,1569],"th",{},"Symptom",[1567,1571,1572],{},"Cause",[1567,1574,1575],{},"Fix",[1577,1578,1579,1595,1619,1634,1648,1659],"tbody",{},[1564,1580,1581,1587,1590],{},[1582,1583,1584,1586],"td",{},[14,1585,26],{}," far below the data",[1582,1588,1589],{},"Formatting applied to empty rows",[1582,1591,1592,1593],{},"Scan values with ",[14,1594,575],{},[1564,1596,1597,1602,1610],{},[1582,1598,1599,1601],{},[14,1600,26],{}," grows while you search",[1582,1603,1604,1606,1607,1609],{},[14,1605,755],{}," or ",[14,1608,561],{}," creates cells",[1582,1611,1612,1613,1615,1616],{},"Use ",[14,1614,751],{},", never probe with ",[14,1617,1618],{},"cell()",[1564,1620,1621,1624,1631],{},[1582,1622,1623],{},"New rows appear after a gap",[1582,1625,1626,1628,1629],{},[14,1627,1051],{}," writes after ",[14,1630,26],{},[1582,1632,1633],{},"Write explicitly after the last data row",[1564,1635,1636,1642,1645],{},[1582,1637,1638,1369,1640],{},[14,1639,26],{},[14,1641,579],{},[1582,1643,1644],{},"Read-only sheet without dimensions",[1582,1646,1647],{},"Scan the rows",[1564,1649,1650,1653,1656],{},[1582,1651,1652],{},"Last row includes notes below the table",[1582,1654,1655],{},"Whole-sheet scan counts any value",[1582,1657,1658],{},"Scan a key column only",[1564,1660,1661,1664,1669],{},[1582,1662,1663],{},"Empty strings counted as data",[1582,1665,1666,1667],{},"Cells written as ",[14,1668,676],{},[1582,1670,1671,1672,1674],{},"Treat ",[14,1673,676],{}," (and optionally whitespace) as empty",[188,1676,1678],{"id":1677},"performance-and-scale-notes","Performance and scale notes",[10,1680,1681,1682,1684,1685,1687],{},"A full scan touches every row once. In normal mode the cells are already in memory, so the scan is fast; the load itself is the cost. In read-only mode the scan streams\nthe XML and is the fastest option for large files — a key-column scan with ",[14,1683,934],{},"\u002F",[14,1686,942],{}," also avoids building tuples for the other columns. If a job appends to the\nsame large sheet repeatedly, store the last row number alongside the file and verify it with a quick check of that row and the next, instead of scanning the whole sheet\nevery time.",[188,1689,1691],{"id":1690},"conclusion","Conclusion",[10,1693,1694,1696,1697,1699,1700,558,1702,1704,1705,1708],{},[14,1695,16],{}," measures how far the sheet's cells extend — including formatting, stale dimensions and cells created just by looking at them — so it is the wrong answer for\n\"where does the data end\". Scan with ",[14,1698,575],{},", over the whole sheet or a key column, treating ",[14,1701,579],{},[14,1703,676],{}," as empty. Write new rows explicitly\nafter that position instead of using ",[14,1706,1707],{},"append()",", use the same scan in read-only mode for large files, and let pandas handle the question when you only need to read.",[188,1710,1712],{"id":1711},"frequently-asked-questions","Frequently asked questions",[10,1714,1715,1719],{},[1716,1717,1718],"strong",{},"Why is openpyxl max_row larger than my data?","\nBecause cells with formatting, or cells your code accessed, count as part of the sheet even when they are empty. max_row reports that extent, not the last value.",[10,1721,1722,1725],{},[1716,1723,1724],{},"Does deleting rows fix max_row?","\nDeleting the formatted empty rows with ws.delete_rows removes those cells, so max_row shrinks accordingly. Clearing their values without removing the formatting does not.",[10,1727,1728,1731],{},[1716,1729,1730],{},"How do I find the last column with data?","\nUse the same scan turned sideways: iterate with iter_cols(values_only=True), or check each row tuple and track the highest index holding a value.",[10,1733,1734,1737],{},[1716,1735,1736],{},"Is there an equivalent of Excel's Ctrl+Up from the bottom?","\nNot directly. Scanning the column with iter_rows and keeping the last filled row gives the same answer without creating cells.",[10,1739,1740,1743],{},[1716,1741,1742],{},"Do formulas that return an empty string count as data?","\nWith a normal load, a formula cell holds its formula text, so it counts as filled. Load with data_only=True to see cached results instead, where a formula that returned an empty string reads as an empty value.",[188,1745,1747],{"id":1746},"related","Related",[1749,1750,1751,1758,1765,1772,1779],"ul",{},[1752,1753,1754,1755,1757],"li",{},"Up one level: ",[19,1756,22],{"href":21}," — reading and editing workbooks cell by cell.",[1752,1759,1760,1764],{},[19,1761,1763],{"href":1762},"\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"," — iter_rows and iter_cols in detail.",[1752,1766,1767,1771],{},[19,1768,1770],{"href":1769},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl\u002F","Insert and Delete Rows and Columns with openpyxl"," — trimming formatted empty rows.",[1752,1773,1774,1778],{},[19,1775,1777],{"href":1776},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas\u002F","Append a DataFrame to an Existing Excel File with pandas"," — the pandas route to adding rows.",[1752,1780,1781,1785],{},[19,1782,1784],{"href":1783},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-and-replace-text-in-excel-with-openpyxl\u002F","Find and Replace Text in Excel with openpyxl"," — another whole-sheet pass that should stay within the data.",[1787,1788,1789],"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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":198,"searchDepth":252,"depth":252,"links":1791},[1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803],{"id":190,"depth":252,"text":191},{"id":425,"depth":252,"text":426},{"id":568,"depth":252,"text":569},{"id":851,"depth":252,"text":852},{"id":1045,"depth":252,"text":1046},{"id":1347,"depth":252,"text":1348},{"id":1469,"depth":252,"text":1470},{"id":1555,"depth":252,"text":1556},{"id":1677,"depth":252,"text":1678},{"id":1690,"depth":252,"text":1691},{"id":1711,"depth":252,"text":1712},{"id":1746,"depth":252,"text":1747},"2026-09-17","ws.max_row counts formatted empty cells, stale dimensions and cells you merely looked at. Find the real last row with data — for the whole sheet or one column — and append in the right place.","md",[1808,1810,1812,1814,1816],{"q":1718,"a":1809},"Because cells with formatting, or cells your code accessed, count as part of the sheet even when they are empty. max_row reports that extent, not the last value.",{"q":1724,"a":1811},"Deleting the formatted empty rows with ws.delete_rows removes those cells, so max_row shrinks accordingly. Clearing their values without removing the formatting does not.",{"q":1730,"a":1813},"Use the same scan turned sideways: iterate with iter_cols(values_only=True), or check each row tuple and track the highest index holding a value.",{"q":1736,"a":1815},"Not directly. Scanning the column with iter_rows and keeping the last filled row gives the same answer without creating cells.",{"q":1742,"a":1817},"With a normal load, a formula cell holds its formula text, so it counts as filled. Load with data_only=True to see cached results instead, where a formula that returned an empty string reads as an empty value.","find the last row with data in openpyxl",null,{"breadcrumb":1821},[1822,1824,1827],{"name":1823,"item":1684},"Home",{"name":1825,"item":1826},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":22,"item":21},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl",{"title":1830,"description":1831},"Find the Last Row with Data in openpyxl (max_row Pitfalls)","Why openpyxl max_row is wrong for formatted or read-only sheets, how to find the last non-empty row or column value, append below real data, and do it fast on large files.","find-the-last-row-with-data-in-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-the-last-row-with-data-in-openpyxl\u002Findex",[1835,1836],"reading-excel","openpyxl","how-to","oons3_obo7Uu8tTtju4N3aTQrslXaGfUE3eLfwxLeYc",[1840,1843],{"title":1784,"path":1841,"stem":1842,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-and-replace-text-in-excel-with-openpyxl","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-and-replace-text-in-excel-with-openpyxl\u002Findex",{"title":1844,"path":1845,"stem":1846,"children":-1},"How openpyxl Stores Python Values: Types, Dates and None","\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fhow-openpyxl-stores-python-values-in-cells","getting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fhow-openpyxl-stores-python-values-in-cells\u002Findex",[1848,1850],{"path":1849,"title":1825},"\u002Fgetting-started-with-python-excel-automation",{"path":1851,"title":22},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation",[],[1854,1857,1861,1864,1867,1870],{"path":1855,"title":1856,"kind":1837},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fread-an-excel-file-from-a-url-or-bytes-in-python","Read an Excel File from a URL or Bytes in Python",{"path":1858,"title":1859,"kind":1860},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas","Reading Excel Files with Pandas","guide",{"path":1862,"title":1863,"kind":1837},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fhow-to-read-excel-with-pandas-step-by-step","How to Read Excel Files With Pandas: A Step-by-Step Guide",{"path":1865,"title":1866,"kind":1837},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fkeep-leading-zeros-when-reading-excel-with-pandas","Keep Leading Zeros When Reading Excel with pandas",{"path":1868,"title":1869,"kind":1837},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Flist-sheet-names-and-column-headers-without-loading-data","List Sheet Names and Column Headers Without Loading the Data",{"path":1871,"title":1872,"kind":1837},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fread-a-cell-range-into-a-pandas-dataframe","Read a Cell Range Like B3:F20 into a pandas DataFrame",1790972069871]