[{"data":1,"prerenderedAt":2339},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas":2330},{"id":4,"title":5,"body":6,"dateModified":2306,"datePublished":2306,"description":2307,"extension":2308,"faq":2309,"meta":2321,"navigation":234,"path":2322,"seo":2323,"slug":2326,"stem":2327,"type":2328,"__hash__":2329},"docs\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas\u002Findex.md","Append a DataFrame to an Existing Excel File with pandas",{"type":7,"value":8,"toc":2294},"minimark",[9,31,170,175,203,206,360,364,369,498,508,694,713,717,724,793,878,890,894,1003,1006,1334,1356,1362,1365,1514,1521,1525,1530,1631,1637,1733,1737,1899,1903,1909,1912,1918,1924,2050,2065,2146,2156,2160,2185,2189,2205,2216,2235,2244,2250,2254,2290],[10,11,12,16,17,20,21,24,25,30],"p",{},[13,14,15],"code",{},"to_excel"," creates a file. Run it twice against the same path and the second run silently discards the first — which is a surprise the first time it costs you a month of accumulated data. Appending needs ",[13,18,19],{},"ExcelWriter"," with ",[13,22,23],{},"mode=\"a\"",", and then a second decision about what happens to a sheet that already exists. This guide covers adding a new sheet, adding rows below existing data, and the engine constraint that makes appending fundamentally different from writing. It extends ",[26,27,29],"a",{"href":28},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002F","Writing DataFrames to Excel with pandas",".",[32,33,42,43,42,47,42,51,42,58,42,65,42,73,42,79,42,85,42,90,42,93,42,97,42,102,42,107,42,112,42,117,42,121,42,124,42,127,42,129,42,132,42,134,42,137,42,141,42,146,42,151,42,154,42,156,42,158,42,161,42,163,42,167],"svg",{"viewBox":34,"role":35,"ariaLabel":36,"ariaLabelledBy":37,"xmlns":40,"style":41},"0 0 800 248","img","Three ExcelWriter behaviours against an existing file: write mode replaces the whole workbook, append mode with replace discards one sheet's contents, and append mode with overlay writes into the sheet leaving existing rows intact.",[38,39],"app-t","app-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[44,45,46],"title",{"id":38},"What each writer mode does to a workbook that already exists",[48,49,50],"desc",{"id":39},"Three outcomes for a workbook containing a Summary sheet and a Detail sheet. Mode w discards the entire file and writes a new one containing only what this call writes. Mode a with if_sheet_exists set to replace keeps the other sheets but empties and rewrites the target sheet. Mode a with overlay keeps everything and writes into the target sheet at the row you specify, which is the only combination that appends rows.",[52,53],"rect",{"x":54,"y":54,"width":55,"height":56,"fill":57},"0","800","248","#ffffff",[59,60,64],"text",{"x":61,"y":62,"style":63},"400","26","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","existing workbook: Summary + Detail",[52,66],{"x":67,"y":68,"width":56,"height":69,"rx":67,"fill":70,"stroke":71,"style":72},"14","42","190","#fee8f2","var(--accent,#f43f8f)","stroke-width:2px",[59,74,78],{"x":75,"y":76,"style":77},"138","70","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","mode=\"w\" (default)",[52,80],{"x":68,"y":81,"width":82,"height":83,"rx":84,"fill":57,"stroke":71},"86","192","30","6",[59,86,89],{"x":75,"y":87,"style":88},"106","font-size:10.5px;fill:var(--accent-ink,#be185d);text-anchor:middle","Summary — gone",[52,91],{"x":68,"y":92,"width":82,"height":83,"rx":84,"fill":57,"stroke":71},"122",[59,94,96],{"x":75,"y":95,"style":88},"142","Detail — gone",[52,98],{"x":68,"y":99,"width":82,"height":83,"rx":84,"fill":100,"stroke":101},"158","#f0f2f5","var(--line,#cdd5e6)",[59,103,106],{"x":75,"y":104,"style":105},"178","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","only the new sheet",[59,108,111],{"x":75,"y":109,"style":110},"212","font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","the whole file is replaced",[52,113],{"x":114,"y":68,"width":56,"height":69,"rx":67,"fill":115,"stroke":116,"style":72},"276","#fdefd8","var(--gold,#b4740a)",[59,118,120],{"x":61,"y":76,"style":119},"font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","mode=\"a\" + \"replace\"",[52,122],{"x":123,"y":81,"width":82,"height":83,"rx":84,"fill":57,"stroke":116},"304",[59,125,126],{"x":61,"y":87,"style":105},"Summary — kept",[52,128],{"x":123,"y":92,"width":82,"height":83,"rx":84,"fill":57,"stroke":71},[59,130,131],{"x":61,"y":95,"style":88},"Detail — emptied",[52,133],{"x":123,"y":99,"width":82,"height":83,"rx":84,"fill":100,"stroke":101},[59,135,136],{"x":61,"y":104,"style":105},"Detail rewritten",[59,138,140],{"x":61,"y":109,"style":139},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","other sheets survive",[52,142],{"x":143,"y":68,"width":56,"height":69,"rx":67,"fill":144,"stroke":145,"style":72},"538","#d9f4f1","var(--teal,#0f9488)",[59,147,150],{"x":148,"y":76,"style":149},"662","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","mode=\"a\" + \"overlay\"",[52,152],{"x":153,"y":81,"width":82,"height":83,"rx":84,"fill":57,"stroke":145},"566",[59,155,126],{"x":148,"y":87,"style":105},[52,157],{"x":153,"y":92,"width":82,"height":83,"rx":84,"fill":57,"stroke":145},[59,159,160],{"x":148,"y":95,"style":105},"Detail — rows 1–40 kept",[52,162],{"x":153,"y":99,"width":82,"height":83,"rx":84,"fill":144,"stroke":145,"style":72},[59,164,166],{"x":148,"y":104,"style":165},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","new rows from 41",[59,168,169],{"x":148,"y":109,"style":165},"the only true append",[171,172,174],"h2",{"id":173},"prerequisites","Prerequisites",[176,177,182],"pre",{"className":178,"code":179,"language":180,"meta":181,"style":181},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[13,183,184],{"__ignoreMap":181},[185,186,189,193,197,200],"span",{"class":187,"line":188},"line",1,[185,190,192],{"class":191},"sMTad","pip",[185,194,196],{"class":195},"srMev"," install",[185,198,199],{"class":195}," pandas",[185,201,202],{"class":195}," openpyxl\n",[10,204,205],{},"A workbook to append to:",[176,207,211],{"className":208,"code":209,"language":210,"meta":181,"style":181},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\nwith pd.ExcelWriter(\"ledger.xlsx\", engine=\"openpyxl\") as writer:\n    pd.DataFrame({\n        \"date\": pd.to_datetime([\"2026-06-01\", \"2026-07-01\"]),\n        \"region\": [\"North\", \"North\"],\n        \"revenue\": [5150.00, 4820.50],\n    }).to_excel(writer, sheet_name=\"Detail\", index=False)\n","python",[13,212,213,229,236,269,275,295,314,333],{"__ignoreMap":181},[185,214,215,219,223,226],{"class":187,"line":188},[185,216,218],{"class":217},"s-kum","import",[185,220,222],{"class":221},"skGVy"," pandas ",[185,224,225],{"class":217},"as",[185,227,228],{"class":221}," pd\n",[185,230,232],{"class":187,"line":231},2,[185,233,235],{"emptyLinePlaceholder":234},true,"\n",[185,237,239,242,245,248,251,255,258,261,264,266],{"class":187,"line":238},3,[185,240,241],{"class":217},"with",[185,243,244],{"class":221}," pd.ExcelWriter(",[185,246,247],{"class":195},"\"ledger.xlsx\"",[185,249,250],{"class":221},", ",[185,252,254],{"class":253},"sa561","engine",[185,256,257],{"class":217},"=",[185,259,260],{"class":195},"\"openpyxl\"",[185,262,263],{"class":221},") ",[185,265,225],{"class":217},[185,267,268],{"class":221}," writer:\n",[185,270,272],{"class":187,"line":271},4,[185,273,274],{"class":221},"    pd.DataFrame({\n",[185,276,278,281,284,287,289,292],{"class":187,"line":277},5,[185,279,280],{"class":195},"        \"date\"",[185,282,283],{"class":221},": pd.to_datetime([",[185,285,286],{"class":195},"\"2026-06-01\"",[185,288,250],{"class":221},[185,290,291],{"class":195},"\"2026-07-01\"",[185,293,294],{"class":221},"]),\n",[185,296,298,301,304,307,309,311],{"class":187,"line":297},6,[185,299,300],{"class":195},"        \"region\"",[185,302,303],{"class":221},": [",[185,305,306],{"class":195},"\"North\"",[185,308,250],{"class":221},[185,310,306],{"class":195},[185,312,313],{"class":221},"],\n",[185,315,317,320,322,326,328,331],{"class":187,"line":316},7,[185,318,319],{"class":195},"        \"revenue\"",[185,321,303],{"class":221},[185,323,325],{"class":324},"sP0c6","5150.00",[185,327,250],{"class":221},[185,329,330],{"class":324},"4820.50",[185,332,313],{"class":221},[185,334,336,339,342,344,347,349,352,354,357],{"class":187,"line":335},8,[185,337,338],{"class":221},"    }).to_excel(writer, ",[185,340,341],{"class":253},"sheet_name",[185,343,257],{"class":217},[185,345,346],{"class":195},"\"Detail\"",[185,348,250],{"class":221},[185,350,351],{"class":253},"index",[185,353,257],{"class":217},[185,355,356],{"class":324},"False",[185,358,359],{"class":221},")\n",[171,361,363],{"id":362},"step-1-add-a-new-sheet-without-losing-the-old-ones","Step 1 — Add a new sheet without losing the old ones",[10,365,366,368],{},[13,367,23],{}," opens the existing workbook rather than creating a new one:",[176,370,372],{"className":208,"code":371,"language":210,"meta":181,"style":181},"import pandas as pd\n\nsummary = pd.DataFrame({\"region\": [\"North\"], \"total\": [9970.50]})\n\nwith pd.ExcelWriter(\"ledger.xlsx\", mode=\"a\", engine=\"openpyxl\") as writer:\n    summary.to_excel(writer, sheet_name=\"Summary\", index=False)\n\nprint(pd.ExcelFile(\"ledger.xlsx\").sheet_names)   # ['Detail', 'Summary']\n",[13,373,374,384,388,419,423,455,477,481],{"__ignoreMap":181},[185,375,376,378,380,382],{"class":187,"line":188},[185,377,218],{"class":217},[185,379,222],{"class":221},[185,381,225],{"class":217},[185,383,228],{"class":221},[185,385,386],{"class":187,"line":231},[185,387,235],{"emptyLinePlaceholder":234},[185,389,390,393,395,398,401,403,405,408,411,413,416],{"class":187,"line":238},[185,391,392],{"class":221},"summary ",[185,394,257],{"class":217},[185,396,397],{"class":221}," pd.DataFrame({",[185,399,400],{"class":195},"\"region\"",[185,402,303],{"class":221},[185,404,306],{"class":195},[185,406,407],{"class":221},"], ",[185,409,410],{"class":195},"\"total\"",[185,412,303],{"class":221},[185,414,415],{"class":324},"9970.50",[185,417,418],{"class":221},"]})\n",[185,420,421],{"class":187,"line":271},[185,422,235],{"emptyLinePlaceholder":234},[185,424,425,427,429,431,433,436,438,441,443,445,447,449,451,453],{"class":187,"line":277},[185,426,241],{"class":217},[185,428,244],{"class":221},[185,430,247],{"class":195},[185,432,250],{"class":221},[185,434,435],{"class":253},"mode",[185,437,257],{"class":217},[185,439,440],{"class":195},"\"a\"",[185,442,250],{"class":221},[185,444,254],{"class":253},[185,446,257],{"class":217},[185,448,260],{"class":195},[185,450,263],{"class":221},[185,452,225],{"class":217},[185,454,268],{"class":221},[185,456,457,460,462,464,467,469,471,473,475],{"class":187,"line":297},[185,458,459],{"class":221},"    summary.to_excel(writer, ",[185,461,341],{"class":253},[185,463,257],{"class":217},[185,465,466],{"class":195},"\"Summary\"",[185,468,250],{"class":221},[185,470,351],{"class":253},[185,472,257],{"class":217},[185,474,356],{"class":324},[185,476,359],{"class":221},[185,478,479],{"class":187,"line":316},[185,480,235],{"emptyLinePlaceholder":234},[185,482,483,486,489,491,494],{"class":187,"line":335},[185,484,485],{"class":324},"print",[185,487,488],{"class":221},"(pd.ExcelFile(",[185,490,247],{"class":195},[185,492,493],{"class":221},").sheet_names)   ",[185,495,497],{"class":496},"s-wDw","# ['Detail', 'Summary']\n",[10,499,500,501,503,504,507],{},"Two things are worth stating plainly. ",[13,502,23],{}," requires the file to exist — it raises ",[13,505,506],{},"FileNotFoundError"," otherwise, which makes a first run of a monthly job fail unless you handle it:",[176,509,511],{"className":208,"code":510,"language":210,"meta":181,"style":181},"from pathlib import Path\nimport pandas as pd\n\ndef write_or_append(df, path, sheet_name):\n    \"\"\"Append to the workbook if it exists, create it if not.\"\"\"\n    path = Path(path)\n    if path.exists():\n        with pd.ExcelWriter(path, mode=\"a\", engine=\"openpyxl\",\n                            if_sheet_exists=\"replace\") as writer:\n            df.to_excel(writer, sheet_name=sheet_name, index=False)\n    else:\n        with pd.ExcelWriter(path, engine=\"openpyxl\") as writer:\n            df.to_excel(writer, sheet_name=sheet_name, index=False)\n    return path\n",[13,512,513,526,536,540,552,557,567,575,600,617,638,647,666,685],{"__ignoreMap":181},[185,514,515,518,521,523],{"class":187,"line":188},[185,516,517],{"class":217},"from",[185,519,520],{"class":221}," pathlib ",[185,522,218],{"class":217},[185,524,525],{"class":221}," Path\n",[185,527,528,530,532,534],{"class":187,"line":231},[185,529,218],{"class":217},[185,531,222],{"class":221},[185,533,225],{"class":217},[185,535,228],{"class":221},[185,537,538],{"class":187,"line":238},[185,539,235],{"emptyLinePlaceholder":234},[185,541,542,545,549],{"class":187,"line":271},[185,543,544],{"class":217},"def",[185,546,548],{"class":547},"s_Opv"," write_or_append",[185,550,551],{"class":221},"(df, path, sheet_name):\n",[185,553,554],{"class":187,"line":277},[185,555,556],{"class":195},"    \"\"\"Append to the workbook if it exists, create it if not.\"\"\"\n",[185,558,559,562,564],{"class":187,"line":297},[185,560,561],{"class":221},"    path ",[185,563,257],{"class":217},[185,565,566],{"class":221}," Path(path)\n",[185,568,569,572],{"class":187,"line":316},[185,570,571],{"class":217},"    if",[185,573,574],{"class":221}," path.exists():\n",[185,576,577,580,583,585,587,589,591,593,595,597],{"class":187,"line":335},[185,578,579],{"class":217},"        with",[185,581,582],{"class":221}," pd.ExcelWriter(path, ",[185,584,435],{"class":253},[185,586,257],{"class":217},[185,588,440],{"class":195},[185,590,250],{"class":221},[185,592,254],{"class":253},[185,594,257],{"class":217},[185,596,260],{"class":195},[185,598,599],{"class":221},",\n",[185,601,603,606,608,611,613,615],{"class":187,"line":602},9,[185,604,605],{"class":253},"                            if_sheet_exists",[185,607,257],{"class":217},[185,609,610],{"class":195},"\"replace\"",[185,612,263],{"class":221},[185,614,225],{"class":217},[185,616,268],{"class":221},[185,618,620,623,625,627,630,632,634,636],{"class":187,"line":619},10,[185,621,622],{"class":221},"            df.to_excel(writer, ",[185,624,341],{"class":253},[185,626,257],{"class":217},[185,628,629],{"class":221},"sheet_name, ",[185,631,351],{"class":253},[185,633,257],{"class":217},[185,635,356],{"class":324},[185,637,359],{"class":221},[185,639,641,644],{"class":187,"line":640},11,[185,642,643],{"class":217},"    else",[185,645,646],{"class":221},":\n",[185,648,650,652,654,656,658,660,662,664],{"class":187,"line":649},12,[185,651,579],{"class":217},[185,653,582],{"class":221},[185,655,254],{"class":253},[185,657,257],{"class":217},[185,659,260],{"class":195},[185,661,263],{"class":221},[185,663,225],{"class":217},[185,665,268],{"class":221},[185,667,669,671,673,675,677,679,681,683],{"class":187,"line":668},13,[185,670,622],{"class":221},[185,672,341],{"class":253},[185,674,257],{"class":217},[185,676,629],{"class":221},[185,678,351],{"class":253},[185,680,257],{"class":217},[185,682,356],{"class":324},[185,684,359],{"class":221},[185,686,688,691],{"class":187,"line":687},14,[185,689,690],{"class":217},"    return",[185,692,693],{"class":221}," path\n",[10,695,696,697,701,702,704,705,708,709,30],{},"And the engine is not a free choice. ",[698,699,700],"strong",{},"xlsxwriter cannot append"," — it only creates files, with no ability to read an existing one. pandas picks openpyxl automatically for ",[13,703,23],{},", but if you name xlsxwriter explicitly you get a ",[13,706,707],{},"ValueError",". The wider trade-off between the two writers is covered in ",[26,710,712],{"href":711},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fopenpyxl-vs-xlsxwriter-vs-pandas-excelwriter\u002F","openpyxl vs xlsxwriter vs pandas.ExcelWriter",[171,714,716],{"id":715},"step-2-choose-what-happens-to-an-existing-sheet","Step 2 — Choose what happens to an existing sheet",[10,718,719,720,723],{},"Writing to a sheet name that already exists needs ",[13,721,722],{},"if_sheet_exists",", and the default raises rather than guessing:",[725,726,727,740],"table",{},[728,729,730],"thead",{},[731,732,733,737],"tr",{},[734,735,736],"th",{},"Value",[734,738,739],{},"Behaviour",[741,742,743,757,766,776],"tbody",{},[731,744,745,752],{},[746,747,748,751],"td",{},[13,749,750],{},"\"error\""," (default)",[746,753,754,755],{},"Raises ",[13,756,707],{},[731,758,759,763],{},[746,760,761],{},[13,762,610],{},[746,764,765],{},"Deletes the sheet and writes a fresh one",[731,767,768,773],{},[746,769,770],{},[13,771,772],{},"\"overlay\"",[746,774,775],{},"Writes into the existing sheet, keeping other cells",[731,777,778,783],{},[746,779,780],{},[13,781,782],{},"\"new\"",[746,784,785,786,250,789,792],{},"Creates ",[13,787,788],{},"Detail1",[13,790,791],{},"Detail2",", …",[176,794,796],{"className":208,"code":795,"language":210,"meta":181,"style":181},"import pandas as pd\n\n# Refresh a sheet completely — the usual choice for a regenerated summary.\nwith pd.ExcelWriter(\"ledger.xlsx\", mode=\"a\", engine=\"openpyxl\",\n                    if_sheet_exists=\"replace\") as writer:\n    summary.to_excel(writer, sheet_name=\"Summary\", index=False)\n",[13,797,798,808,812,817,843,858],{"__ignoreMap":181},[185,799,800,802,804,806],{"class":187,"line":188},[185,801,218],{"class":217},[185,803,222],{"class":221},[185,805,225],{"class":217},[185,807,228],{"class":221},[185,809,810],{"class":187,"line":231},[185,811,235],{"emptyLinePlaceholder":234},[185,813,814],{"class":187,"line":238},[185,815,816],{"class":496},"# Refresh a sheet completely — the usual choice for a regenerated summary.\n",[185,818,819,821,823,825,827,829,831,833,835,837,839,841],{"class":187,"line":271},[185,820,241],{"class":217},[185,822,244],{"class":221},[185,824,247],{"class":195},[185,826,250],{"class":221},[185,828,435],{"class":253},[185,830,257],{"class":217},[185,832,440],{"class":195},[185,834,250],{"class":221},[185,836,254],{"class":253},[185,838,257],{"class":217},[185,840,260],{"class":195},[185,842,599],{"class":221},[185,844,845,848,850,852,854,856],{"class":187,"line":277},[185,846,847],{"class":253},"                    if_sheet_exists",[185,849,257],{"class":217},[185,851,610],{"class":195},[185,853,263],{"class":221},[185,855,225],{"class":217},[185,857,268],{"class":221},[185,859,860,862,864,866,868,870,872,874,876],{"class":187,"line":297},[185,861,459],{"class":221},[185,863,341],{"class":253},[185,865,257],{"class":217},[185,867,466],{"class":195},[185,869,250],{"class":221},[185,871,351],{"class":253},[185,873,257],{"class":217},[185,875,356],{"class":324},[185,877,359],{"class":221},[10,879,880,882,883,885,886,889],{},[13,881,610],{}," is right for a derived sheet you regenerate each run. ",[13,884,772],{}," is the one that actually appends rows, and it needs a ",[13,887,888],{},"startrow"," to say where.",[171,891,893],{"id":892},"step-3-append-rows-below-the-existing-data","Step 3 — Append rows below the existing data",[32,895,42,901,42,904,42,907,42,910,42,915,42,923,42,929,42,933,42,936,42,940,42,945,42,948,42,952,42,956,42,959,42,963,42,967,42,972,42,978,42,981,42,986,42,989,42,994,42,999],{"viewBox":896,"role":35,"ariaLabel":897,"ariaLabelledBy":898,"xmlns":40,"style":41},"0 0 800 244","The startrow arithmetic: max_row is one-based and startrow is zero-based, so passing max_row directly lands the first new row immediately after the last existing one.",[899,900],"sr-t","sr-d",[44,902,903],{"id":899},"Getting the startrow arithmetic right",[48,905,906],{"id":900},"A sheet whose header is row 1 and whose data occupies rows 2 and 3, so max_row is 3. Because startrow is zero-based while max_row is one-based, passing startrow equal to max_row places the first new row at sheet row 4 — immediately after the last existing row. Passing max_row plus one leaves a blank row 4, and passing max_row minus one overwrites the last row of existing data.",[52,908],{"x":54,"y":54,"width":55,"height":909,"fill":57},"244",[59,911,914],{"x":912,"y":912,"style":913},"46","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","1",[52,916],{"x":917,"y":83,"width":918,"height":919,"rx":920,"fill":921,"stroke":922,"style":72},"66","270","28","5","#ebebfd","var(--brand,#5b5cf0)",[59,924,928],{"x":925,"y":926,"style":927},"201","49","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","header: date · region · revenue",[59,930,932],{"x":912,"y":931,"style":913},"80","2",[52,934],{"x":917,"y":935,"width":918,"height":919,"rx":920,"fill":57,"stroke":101},"64",[59,937,939],{"x":925,"y":938,"style":105},"83","2026-06-01 · North · 5150.00",[59,941,944],{"x":912,"y":942,"style":943},"114","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","3",[52,946],{"x":917,"y":947,"width":918,"height":919,"rx":920,"fill":57,"stroke":101,"style":72},"98",[59,949,951],{"x":925,"y":950,"style":105},"117","2026-07-01 · North · 4820.50",[59,953,955],{"x":912,"y":954,"style":913},"148","4",[52,957],{"x":917,"y":958,"width":918,"height":919,"rx":920,"fill":144,"stroke":145,"style":72},"132",[59,960,962],{"x":925,"y":961,"style":165},"151","the new row belongs here",[59,964,966],{"x":925,"y":82,"style":965},"font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","ws.max_row == 3 (one-based)",[52,968],{"x":969,"y":83,"width":970,"height":68,"rx":971,"fill":144,"stroke":145,"style":72},"368","416","9",[59,973,977],{"x":974,"y":975,"style":976},"392","56","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157)","startrow=3 → lands on sheet row 4 — correct",[52,979],{"x":969,"y":980,"width":970,"height":68,"rx":971,"fill":115,"stroke":116,"style":72},"82",[59,982,985],{"x":974,"y":983,"style":984},"108","font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06)","startrow=4 → leaves row 4 blank",[52,987],{"x":969,"y":988,"width":970,"height":68,"rx":971,"fill":70,"stroke":71,"style":72},"134",[59,990,993],{"x":974,"y":991,"style":992},"160","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d)","startrow=2 → overwrites the July row",[59,995,998],{"x":996,"y":997,"style":913},"576","204","and always pass header=False, or the column names",[59,1000,1002],{"x":996,"y":1001,"style":913},"222","reappear in the middle of the data",[10,1004,1005],{},"Find the last used row, then write below it with no header:",[176,1007,1009],{"className":208,"code":1008,"language":210,"meta":181,"style":181},"import pandas as pd\nfrom openpyxl import load_workbook\n\ndef append_rows(path, sheet_name, df):\n    \"\"\"Add df's rows below whatever is already in the sheet.\"\"\"\n    book = load_workbook(path)\n    if sheet_name not in book.sheetnames:\n        raise KeyError(f\"{sheet_name!r} is not in {path}\")\n    start = book[sheet_name].max_row          # 1-based; next free row index\n    book.close()\n\n    with pd.ExcelWriter(path, mode=\"a\", engine=\"openpyxl\",\n                        if_sheet_exists=\"overlay\") as writer:\n        df.to_excel(\n            writer,\n            sheet_name=sheet_name,\n            index=False,\n            header=False,        # the header is already there\n            startrow=start,      # 0-based here, so max_row lands one row below\n        )\n    return start\n\nnew_rows = pd.DataFrame({\n    \"date\": pd.to_datetime([\"2026-08-01\"]),\n    \"region\": [\"North\"],\n    \"revenue\": [5402.75],\n})\nappend_rows(\"ledger.xlsx\", \"Detail\", new_rows)\n",[13,1010,1011,1021,1033,1037,1047,1052,1062,1078,1121,1134,1139,1143,1166,1181,1186,1192,1203,1215,1231,1245,1251,1259,1264,1275,1288,1300,1313,1319],{"__ignoreMap":181},[185,1012,1013,1015,1017,1019],{"class":187,"line":188},[185,1014,218],{"class":217},[185,1016,222],{"class":221},[185,1018,225],{"class":217},[185,1020,228],{"class":221},[185,1022,1023,1025,1028,1030],{"class":187,"line":231},[185,1024,517],{"class":217},[185,1026,1027],{"class":221}," openpyxl ",[185,1029,218],{"class":217},[185,1031,1032],{"class":221}," load_workbook\n",[185,1034,1035],{"class":187,"line":238},[185,1036,235],{"emptyLinePlaceholder":234},[185,1038,1039,1041,1044],{"class":187,"line":271},[185,1040,544],{"class":217},[185,1042,1043],{"class":547}," append_rows",[185,1045,1046],{"class":221},"(path, sheet_name, df):\n",[185,1048,1049],{"class":187,"line":277},[185,1050,1051],{"class":195},"    \"\"\"Add df's rows below whatever is already in the sheet.\"\"\"\n",[185,1053,1054,1057,1059],{"class":187,"line":297},[185,1055,1056],{"class":221},"    book ",[185,1058,257],{"class":217},[185,1060,1061],{"class":221}," load_workbook(path)\n",[185,1063,1064,1066,1069,1072,1075],{"class":187,"line":316},[185,1065,571],{"class":217},[185,1067,1068],{"class":221}," sheet_name ",[185,1070,1071],{"class":217},"not",[185,1073,1074],{"class":217}," in",[185,1076,1077],{"class":221}," book.sheetnames:\n",[185,1079,1080,1083,1086,1089,1092,1095,1099,1101,1104,1107,1110,1112,1115,1117,1119],{"class":187,"line":335},[185,1081,1082],{"class":217},"        raise",[185,1084,1085],{"class":324}," KeyError",[185,1087,1088],{"class":221},"(",[185,1090,1091],{"class":217},"f",[185,1093,1094],{"class":195},"\"",[185,1096,1098],{"class":1097},"sSjpA","{",[185,1100,341],{"class":221},[185,1102,1103],{"class":217},"!r",[185,1105,1106],{"class":1097},"}",[185,1108,1109],{"class":195}," is not in ",[185,1111,1098],{"class":1097},[185,1113,1114],{"class":221},"path",[185,1116,1106],{"class":1097},[185,1118,1094],{"class":195},[185,1120,359],{"class":221},[185,1122,1123,1126,1128,1131],{"class":187,"line":602},[185,1124,1125],{"class":221},"    start ",[185,1127,257],{"class":217},[185,1129,1130],{"class":221}," book[sheet_name].max_row          ",[185,1132,1133],{"class":496},"# 1-based; next free row index\n",[185,1135,1136],{"class":187,"line":619},[185,1137,1138],{"class":221},"    book.close()\n",[185,1140,1141],{"class":187,"line":640},[185,1142,235],{"emptyLinePlaceholder":234},[185,1144,1145,1148,1150,1152,1154,1156,1158,1160,1162,1164],{"class":187,"line":649},[185,1146,1147],{"class":217},"    with",[185,1149,582],{"class":221},[185,1151,435],{"class":253},[185,1153,257],{"class":217},[185,1155,440],{"class":195},[185,1157,250],{"class":221},[185,1159,254],{"class":253},[185,1161,257],{"class":217},[185,1163,260],{"class":195},[185,1165,599],{"class":221},[185,1167,1168,1171,1173,1175,1177,1179],{"class":187,"line":668},[185,1169,1170],{"class":253},"                        if_sheet_exists",[185,1172,257],{"class":217},[185,1174,772],{"class":195},[185,1176,263],{"class":221},[185,1178,225],{"class":217},[185,1180,268],{"class":221},[185,1182,1183],{"class":187,"line":687},[185,1184,1185],{"class":221},"        df.to_excel(\n",[185,1187,1189],{"class":187,"line":1188},15,[185,1190,1191],{"class":221},"            writer,\n",[185,1193,1195,1198,1200],{"class":187,"line":1194},16,[185,1196,1197],{"class":253},"            sheet_name",[185,1199,257],{"class":217},[185,1201,1202],{"class":221},"sheet_name,\n",[185,1204,1206,1209,1211,1213],{"class":187,"line":1205},17,[185,1207,1208],{"class":253},"            index",[185,1210,257],{"class":217},[185,1212,356],{"class":324},[185,1214,599],{"class":221},[185,1216,1218,1221,1223,1225,1228],{"class":187,"line":1217},18,[185,1219,1220],{"class":253},"            header",[185,1222,257],{"class":217},[185,1224,356],{"class":324},[185,1226,1227],{"class":221},",        ",[185,1229,1230],{"class":496},"# the header is already there\n",[185,1232,1234,1237,1239,1242],{"class":187,"line":1233},19,[185,1235,1236],{"class":253},"            startrow",[185,1238,257],{"class":217},[185,1240,1241],{"class":221},"start,      ",[185,1243,1244],{"class":496},"# 0-based here, so max_row lands one row below\n",[185,1246,1248],{"class":187,"line":1247},20,[185,1249,1250],{"class":221},"        )\n",[185,1252,1254,1256],{"class":187,"line":1253},21,[185,1255,690],{"class":217},[185,1257,1258],{"class":221}," start\n",[185,1260,1262],{"class":187,"line":1261},22,[185,1263,235],{"emptyLinePlaceholder":234},[185,1265,1267,1270,1272],{"class":187,"line":1266},23,[185,1268,1269],{"class":221},"new_rows ",[185,1271,257],{"class":217},[185,1273,1274],{"class":221}," pd.DataFrame({\n",[185,1276,1278,1281,1283,1286],{"class":187,"line":1277},24,[185,1279,1280],{"class":195},"    \"date\"",[185,1282,283],{"class":221},[185,1284,1285],{"class":195},"\"2026-08-01\"",[185,1287,294],{"class":221},[185,1289,1291,1294,1296,1298],{"class":187,"line":1290},25,[185,1292,1293],{"class":195},"    \"region\"",[185,1295,303],{"class":221},[185,1297,306],{"class":195},[185,1299,313],{"class":221},[185,1301,1303,1306,1308,1311],{"class":187,"line":1302},26,[185,1304,1305],{"class":195},"    \"revenue\"",[185,1307,303],{"class":221},[185,1309,1310],{"class":324},"5402.75",[185,1312,313],{"class":221},[185,1314,1316],{"class":187,"line":1315},27,[185,1317,1318],{"class":221},"})\n",[185,1320,1322,1325,1327,1329,1331],{"class":187,"line":1321},28,[185,1323,1324],{"class":221},"append_rows(",[185,1326,247],{"class":195},[185,1328,250],{"class":221},[185,1330,346],{"class":195},[185,1332,1333],{"class":221},", new_rows)\n",[10,1335,1336,1337,1340,1341,1343,1344,1347,1348,1351,1352,1355],{},"The index arithmetic is the part to get right and easy to get wrong. ",[13,1338,1339],{},"ws.max_row"," is 1-based, and ",[13,1342,888],{}," is 0-based, so passing ",[13,1345,1346],{},"max_row"," directly places the first new row immediately after the last existing one. Passing ",[13,1349,1350],{},"max_row + 1"," leaves a blank row; passing ",[13,1353,1354],{},"max_row - 1"," overwrites the last row of existing data.",[10,1357,1358,1361],{},[13,1359,1360],{},"header=False"," matters just as much. Without it, the column names are written again in the middle of the data, and every downstream read treats that row as a record.",[10,1363,1364],{},"Guard against duplicates. An append with no key check will happily add August twice when a job retries:",[176,1366,1368],{"className":208,"code":1367,"language":210,"meta":181,"style":181},"import pandas as pd\n\ndef append_new_periods(path, sheet_name, df, key=\"date\"):\n    \"\"\"Append only the rows whose key is not already present.\"\"\"\n    existing = pd.read_excel(path, sheet_name=sheet_name)\n    already = set(existing[key].astype(str))\n\n    fresh = df[~df[key].astype(str).isin(already)]\n    if fresh.empty:\n        print(\"nothing new to append\")\n        return 0\n\n    append_rows(path, sheet_name, fresh)\n    return len(fresh)\n",[13,1369,1370,1380,1384,1402,1407,1424,1443,1447,1468,1475,1487,1495,1499,1504],{"__ignoreMap":181},[185,1371,1372,1374,1376,1378],{"class":187,"line":188},[185,1373,218],{"class":217},[185,1375,222],{"class":221},[185,1377,225],{"class":217},[185,1379,228],{"class":221},[185,1381,1382],{"class":187,"line":231},[185,1383,235],{"emptyLinePlaceholder":234},[185,1385,1386,1388,1391,1394,1396,1399],{"class":187,"line":238},[185,1387,544],{"class":217},[185,1389,1390],{"class":547}," append_new_periods",[185,1392,1393],{"class":221},"(path, sheet_name, df, key",[185,1395,257],{"class":217},[185,1397,1398],{"class":195},"\"date\"",[185,1400,1401],{"class":221},"):\n",[185,1403,1404],{"class":187,"line":271},[185,1405,1406],{"class":195},"    \"\"\"Append only the rows whose key is not already present.\"\"\"\n",[185,1408,1409,1412,1414,1417,1419,1421],{"class":187,"line":277},[185,1410,1411],{"class":221},"    existing ",[185,1413,257],{"class":217},[185,1415,1416],{"class":221}," pd.read_excel(path, ",[185,1418,341],{"class":253},[185,1420,257],{"class":217},[185,1422,1423],{"class":221},"sheet_name)\n",[185,1425,1426,1429,1431,1434,1437,1440],{"class":187,"line":297},[185,1427,1428],{"class":221},"    already ",[185,1430,257],{"class":217},[185,1432,1433],{"class":324}," set",[185,1435,1436],{"class":221},"(existing[key].astype(",[185,1438,1439],{"class":324},"str",[185,1441,1442],{"class":221},"))\n",[185,1444,1445],{"class":187,"line":316},[185,1446,235],{"emptyLinePlaceholder":234},[185,1448,1449,1452,1454,1457,1460,1463,1465],{"class":187,"line":335},[185,1450,1451],{"class":221},"    fresh ",[185,1453,257],{"class":217},[185,1455,1456],{"class":221}," df[",[185,1458,1459],{"class":217},"~",[185,1461,1462],{"class":221},"df[key].astype(",[185,1464,1439],{"class":324},[185,1466,1467],{"class":221},").isin(already)]\n",[185,1469,1470,1472],{"class":187,"line":602},[185,1471,571],{"class":217},[185,1473,1474],{"class":221}," fresh.empty:\n",[185,1476,1477,1480,1482,1485],{"class":187,"line":619},[185,1478,1479],{"class":324},"        print",[185,1481,1088],{"class":221},[185,1483,1484],{"class":195},"\"nothing new to append\"",[185,1486,359],{"class":221},[185,1488,1489,1492],{"class":187,"line":640},[185,1490,1491],{"class":217},"        return",[185,1493,1494],{"class":324}," 0\n",[185,1496,1497],{"class":187,"line":649},[185,1498,235],{"emptyLinePlaceholder":234},[185,1500,1501],{"class":187,"line":668},[185,1502,1503],{"class":221},"    append_rows(path, sheet_name, fresh)\n",[185,1505,1506,1508,1511],{"class":187,"line":687},[185,1507,690],{"class":217},[185,1509,1510],{"class":324}," len",[185,1512,1513],{"class":221},"(fresh)\n",[10,1515,1516,1517,30],{},"That idempotency is what makes a scheduled append safe to re-run, in the same spirit as the retry patterns in ",[26,1518,1520],{"href":1519},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fretry-a-failed-excel-report-job-in-python\u002F","retrying a failed Excel report job",[171,1522,1524],{"id":1523},"step-4-know-what-appending-costs-you","Step 4 — Know what appending costs you",[10,1526,1527,1529],{},[13,1528,23],{}," opens the workbook through openpyxl, which parses the whole file and writes the whole file back. Nothing is written incrementally.",[32,1531,42,1537,42,1540,42,1543,42,1546,42,1549,42,1553,42,1558,42,1607,42,1611,42,1614,42,1618,42,1622,42,1627],{"viewBox":1532,"role":35,"ariaLabel":1533,"ariaLabelledBy":1534,"xmlns":40,"style":41},"0 0 800 236","Appending in a loop re-parses and rewrites the entire workbook each time, so cost grows with both the number of appends and the accumulated file size, while accumulating in memory and writing once is linear.",[1535,1536],"appcost-t","appcost-d",[44,1538,1539],{"id":1535},"Why appending in a loop gets slower with every iteration",[48,1541,1542],{"id":1536},"Twelve monthly appends. In the looped version each append re-parses and rewrites the entire workbook, so each iteration is more expensive than the last as the file grows — the bars get progressively wider. In the accumulate-then-write version, twelve months of rows are collected in memory and the workbook is written once, so there is a single bar regardless of how many months are involved.",[52,1544],{"x":54,"y":54,"width":55,"height":1545,"fill":57},"236",[59,1547,1548],{"x":61,"y":62,"style":63},"twelve monthly appends",[52,1550],{"x":67,"y":1551,"width":1552,"height":919,"rx":84,"fill":100,"stroke":101},"52","128",[59,1554,1557],{"x":1555,"y":1556,"style":943},"78","71","append monthly",[1559,1560,1562,1563,1562,1566,1562,1570,1562,1573,1562,1577,1562,1581,1562,1585,1562,1589,1562,1592,1562,1596,1562,1600,1562,1604,42],"g",{"fill":1561},"#f43f8f","\n    ",[52,1564],{"x":1565,"y":1551,"width":67,"height":919,"rx":944},"144",[52,1567],{"x":1568,"y":1551,"width":1569,"height":919,"rx":944},"162","20",[52,1571],{"x":1572,"y":1551,"width":62,"height":919,"rx":944},"186",[52,1574],{"x":1575,"y":1551,"width":1576,"height":919,"rx":944},"216","32",[52,1578],{"x":1579,"y":1551,"width":1580,"height":919,"rx":944},"252","38",[52,1582],{"x":1583,"y":1551,"width":1584,"height":919,"rx":944},"294","44",[52,1586],{"x":1587,"y":1551,"width":1588,"height":919,"rx":944},"342","50",[52,1590],{"x":1591,"y":1551,"width":975,"height":919,"rx":944},"396",[52,1593],{"x":1594,"y":1551,"width":1595,"height":919,"rx":944},"456","62",[52,1597],{"x":1598,"y":1551,"width":1599,"height":919,"rx":944},"522","68",[52,1601],{"x":1602,"y":1551,"width":1603,"height":919,"rx":944},"594","74",[52,1605],{"x":1606,"y":1551,"width":931,"height":919,"rx":944},"672",[59,1608,1610],{"x":61,"y":1609,"style":913},"104","each bar is one full parse and rewrite of a workbook that keeps growing",[52,1612],{"x":67,"y":1613,"width":1552,"height":919,"rx":84,"fill":100,"stroke":101},"136",[59,1615,1617],{"x":1555,"y":1616,"style":943},"155","write once",[52,1619],{"x":1565,"y":1613,"width":1620,"height":919,"rx":944,"fill":1621},"96","#0f9488",[59,1623,1626],{"x":1624,"y":1625,"style":976},"256","156","one write, whatever the month count",[59,1628,1630],{"x":61,"y":997,"style":1629},"font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","append when a human adds to a living workbook; accumulate when a script owns it",[10,1632,1633,1634],{},"The rule that follows: ",[698,1635,1636],{},"append when a workbook is a living document you add to occasionally, and accumulate when a script owns the whole file.",[176,1638,1640],{"className":208,"code":1639,"language":210,"meta":181,"style":181},"import pandas as pd\n\n# Owned by the script: collect everything, write once.\nmonthly = [load_month(m) for m in months]\npd.concat(monthly, ignore_index=True).to_excel(\n    \"ledger.xlsx\", sheet_name=\"Detail\", index=False, engine=\"xlsxwriter\"\n)\n",[13,1641,1642,1652,1656,1661,1683,1699,1729],{"__ignoreMap":181},[185,1643,1644,1646,1648,1650],{"class":187,"line":188},[185,1645,218],{"class":217},[185,1647,222],{"class":221},[185,1649,225],{"class":217},[185,1651,228],{"class":221},[185,1653,1654],{"class":187,"line":231},[185,1655,235],{"emptyLinePlaceholder":234},[185,1657,1658],{"class":187,"line":238},[185,1659,1660],{"class":496},"# Owned by the script: collect everything, write once.\n",[185,1662,1663,1666,1668,1671,1674,1677,1680],{"class":187,"line":271},[185,1664,1665],{"class":221},"monthly ",[185,1667,257],{"class":217},[185,1669,1670],{"class":221}," [load_month(m) ",[185,1672,1673],{"class":217},"for",[185,1675,1676],{"class":221}," m ",[185,1678,1679],{"class":217},"in",[185,1681,1682],{"class":221}," months]\n",[185,1684,1685,1688,1691,1693,1696],{"class":187,"line":277},[185,1686,1687],{"class":221},"pd.concat(monthly, ",[185,1689,1690],{"class":253},"ignore_index",[185,1692,257],{"class":217},[185,1694,1695],{"class":324},"True",[185,1697,1698],{"class":221},").to_excel(\n",[185,1700,1701,1704,1706,1708,1710,1712,1714,1716,1718,1720,1722,1724,1726],{"class":187,"line":297},[185,1702,1703],{"class":195},"    \"ledger.xlsx\"",[185,1705,250],{"class":221},[185,1707,341],{"class":253},[185,1709,257],{"class":217},[185,1711,346],{"class":195},[185,1713,250],{"class":221},[185,1715,351],{"class":253},[185,1717,257],{"class":217},[185,1719,356],{"class":324},[185,1721,250],{"class":221},[185,1723,254],{"class":253},[185,1725,257],{"class":217},[185,1727,1728],{"class":195},"\"xlsxwriter\"\n",[185,1730,1731],{"class":187,"line":316},[185,1732,359],{"class":221},[171,1734,1736],{"id":1735},"common-pitfalls-and-fixes","Common pitfalls and fixes",[725,1738,1739,1752],{},[728,1740,1741],{},[731,1742,1743,1746,1749],{},[734,1744,1745],{},"Symptom",[734,1747,1748],{},"Cause",[734,1750,1751],{},"Fix",[741,1753,1754,1770,1786,1804,1829,1844,1859,1873,1884],{},[731,1755,1756,1759,1765],{},[746,1757,1758],{},"Existing sheets vanished",[746,1760,1761,1762],{},"Default ",[13,1763,1764],{},"mode=\"w\"",[746,1766,1767,1768,30],{},"Pass ",[13,1769,23],{},[731,1771,1772,1777,1780],{},[746,1773,1774],{},[13,1775,1776],{},"ValueError: Append mode is not supported with xlsxwriter",[746,1778,1779],{},"Wrong engine",[746,1781,1782,1783,30],{},"Use ",[13,1784,1785],{},"engine=\"openpyxl\"",[731,1787,1788,1793,1798],{},[746,1789,1790,1792],{},[13,1791,506],{}," on the first run",[746,1794,1795,1797],{},[13,1796,23],{}," needs the file to exist",[746,1799,1800,1801,30],{},"Branch on ",[13,1802,1803],{},"path.exists()",[731,1805,1806,1811,1816],{},[746,1807,1808],{},[13,1809,1810],{},"ValueError: Sheet 'X' already exists",[746,1812,1813,1815],{},[13,1814,722],{}," not set",[746,1817,1818,1819,250,1822,1825,1826,30],{},"Choose ",[13,1820,1821],{},"replace",[13,1823,1824],{},"overlay"," or ",[13,1827,1828],{},"new",[731,1830,1831,1834,1839],{},[746,1832,1833],{},"Header repeated mid-sheet",[746,1835,1836,1838],{},[13,1837,1360],{}," omitted",[746,1840,1767,1841,1843],{},[13,1842,1360],{}," when appending rows.",[731,1845,1846,1849,1854],{},[746,1847,1848],{},"Blank row between old and new data",[746,1850,1851],{},[13,1852,1853],{},"startrow=max_row + 1",[746,1855,1782,1856,30],{},[13,1857,1858],{},"startrow=max_row",[731,1860,1861,1864,1869],{},[746,1862,1863],{},"Last existing row overwritten",[746,1865,1866],{},[13,1867,1868],{},"startrow=max_row - 1",[746,1870,1782,1871,30],{},[13,1872,1858],{},[731,1874,1875,1878,1881],{},[746,1876,1877],{},"Duplicate months after a retry",[746,1879,1880],{},"No key check",[746,1882,1883],{},"Filter against the keys already present.",[731,1885,1886,1889,1894],{},[746,1887,1888],{},"Formatting lost on the target sheet",[746,1890,1891],{},[13,1892,1893],{},"if_sheet_exists=\"replace\"",[746,1895,1782,1896,1898],{},[13,1897,1824],{},", or re-apply the formatting.",[171,1900,1902],{"id":1901},"performance-and-scale-notes","Performance and scale notes",[10,1904,1905,1906,1908],{},"Every ",[13,1907,23],{}," write is a full read-modify-write of the workbook. On a 20 MB ledger that is seconds per append, and twelve appends over a year cost more than a hundred times a single write of the same data.",[10,1910,1911],{},"Three alternatives when volume matters.",[10,1913,1914,1917],{},[698,1915,1916],{},"Accumulate in memory and write once",", as above. This is the right answer whenever a script owns the file.",[10,1919,1920,1923],{},[698,1921,1922],{},"Append to CSV, convert at the end."," CSV is genuinely append-only, so adding rows is O(rows added) regardless of file size:",[176,1925,1927],{"className":208,"code":1926,"language":210,"meta":181,"style":181},"import pandas as pd\nfrom pathlib import Path\n\ndef append_csv(df, path):\n    \"\"\"Truly incremental — no re-parse of what is already there.\"\"\"\n    path = Path(path)\n    df.to_csv(path, mode=\"a\", header=not path.exists(), index=False)\n\n# At the end of the period, convert once.\npd.read_csv(\"ledger.csv\").to_excel(\"ledger.xlsx\", index=False,\n                                   engine=\"xlsxwriter\")\n",[13,1928,1929,1939,1949,1953,1963,1968,1976,2006,2010,2015,2038],{"__ignoreMap":181},[185,1930,1931,1933,1935,1937],{"class":187,"line":188},[185,1932,218],{"class":217},[185,1934,222],{"class":221},[185,1936,225],{"class":217},[185,1938,228],{"class":221},[185,1940,1941,1943,1945,1947],{"class":187,"line":231},[185,1942,517],{"class":217},[185,1944,520],{"class":221},[185,1946,218],{"class":217},[185,1948,525],{"class":221},[185,1950,1951],{"class":187,"line":238},[185,1952,235],{"emptyLinePlaceholder":234},[185,1954,1955,1957,1960],{"class":187,"line":271},[185,1956,544],{"class":217},[185,1958,1959],{"class":547}," append_csv",[185,1961,1962],{"class":221},"(df, path):\n",[185,1964,1965],{"class":187,"line":277},[185,1966,1967],{"class":195},"    \"\"\"Truly incremental — no re-parse of what is already there.\"\"\"\n",[185,1969,1970,1972,1974],{"class":187,"line":297},[185,1971,561],{"class":221},[185,1973,257],{"class":217},[185,1975,566],{"class":221},[185,1977,1978,1981,1983,1985,1987,1989,1992,1995,1998,2000,2002,2004],{"class":187,"line":316},[185,1979,1980],{"class":221},"    df.to_csv(path, ",[185,1982,435],{"class":253},[185,1984,257],{"class":217},[185,1986,440],{"class":195},[185,1988,250],{"class":221},[185,1990,1991],{"class":253},"header",[185,1993,1994],{"class":217},"=not",[185,1996,1997],{"class":221}," path.exists(), ",[185,1999,351],{"class":253},[185,2001,257],{"class":217},[185,2003,356],{"class":324},[185,2005,359],{"class":221},[185,2007,2008],{"class":187,"line":335},[185,2009,235],{"emptyLinePlaceholder":234},[185,2011,2012],{"class":187,"line":602},[185,2013,2014],{"class":496},"# At the end of the period, convert once.\n",[185,2016,2017,2020,2023,2026,2028,2030,2032,2034,2036],{"class":187,"line":619},[185,2018,2019],{"class":221},"pd.read_csv(",[185,2021,2022],{"class":195},"\"ledger.csv\"",[185,2024,2025],{"class":221},").to_excel(",[185,2027,247],{"class":195},[185,2029,250],{"class":221},[185,2031,351],{"class":253},[185,2033,257],{"class":217},[185,2035,356],{"class":324},[185,2037,599],{"class":221},[185,2039,2040,2043,2045,2048],{"class":187,"line":640},[185,2041,2042],{"class":253},"                                   engine",[185,2044,257],{"class":217},[185,2046,2047],{"class":195},"\"xlsxwriter\"",[185,2049,359],{"class":221},[10,2051,2052,2055,2056,2059,2060,2064],{},[698,2053,2054],{},"Write one sheet per period"," instead of appending to one growing sheet. Each write touches only the new sheet, and reading them back is a single call with ",[13,2057,2058],{},"sheet_name=None",", as described in ",[26,2061,2063],{"href":2062},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fread-all-sheets-from-an-excel-file-into-dataframes\u002F","reading all sheets into DataFrames",":",[176,2066,2068],{"className":208,"code":2067,"language":210,"meta":181,"style":181},"import pandas as pd\n\nwith pd.ExcelWriter(\"ledger.xlsx\", mode=\"a\", engine=\"openpyxl\",\n                    if_sheet_exists=\"replace\") as writer:\n    august.to_excel(writer, sheet_name=\"2026-08\", index=False)\n",[13,2069,2070,2080,2084,2110,2124],{"__ignoreMap":181},[185,2071,2072,2074,2076,2078],{"class":187,"line":188},[185,2073,218],{"class":217},[185,2075,222],{"class":221},[185,2077,225],{"class":217},[185,2079,228],{"class":221},[185,2081,2082],{"class":187,"line":231},[185,2083,235],{"emptyLinePlaceholder":234},[185,2085,2086,2088,2090,2092,2094,2096,2098,2100,2102,2104,2106,2108],{"class":187,"line":238},[185,2087,241],{"class":217},[185,2089,244],{"class":221},[185,2091,247],{"class":195},[185,2093,250],{"class":221},[185,2095,435],{"class":253},[185,2097,257],{"class":217},[185,2099,440],{"class":195},[185,2101,250],{"class":221},[185,2103,254],{"class":253},[185,2105,257],{"class":217},[185,2107,260],{"class":195},[185,2109,599],{"class":221},[185,2111,2112,2114,2116,2118,2120,2122],{"class":187,"line":271},[185,2113,847],{"class":253},[185,2115,257],{"class":217},[185,2117,610],{"class":195},[185,2119,263],{"class":221},[185,2121,225],{"class":217},[185,2123,268],{"class":221},[185,2125,2126,2129,2131,2133,2136,2138,2140,2142,2144],{"class":187,"line":277},[185,2127,2128],{"class":221},"    august.to_excel(writer, ",[185,2130,341],{"class":253},[185,2132,257],{"class":217},[185,2134,2135],{"class":195},"\"2026-08\"",[185,2137,250],{"class":221},[185,2139,351],{"class":253},[185,2141,257],{"class":217},[185,2143,356],{"class":324},[185,2145,359],{"class":221},[10,2147,2148,2149,2151,2152,2155],{},"One memory note: ",[13,2150,23],{}," holds the entire existing workbook in memory while it writes, so peak usage is roughly the whole file plus the new data. openpyxl's streaming ",[13,2153,2154],{},"write_only"," mode is not available here — it builds a new workbook and therefore cannot append — so for genuinely large accumulating datasets, the CSV route or one-sheet-per-period is the only approach that stays flat.",[171,2157,2159],{"id":2158},"conclusion","Conclusion",[10,2161,2162,2163,2166,2167,2169,2170,2172,2173,20,2175,2178,2179,2181,2182,2184],{},"Appending to an existing workbook is ",[13,2164,2165],{},"pd.ExcelWriter(path, mode=\"a\", engine=\"openpyxl\")"," plus a decision about ",[13,2168,722],{},". Use ",[13,2171,610],{}," for a derived sheet you regenerate, and ",[13,2174,772],{},[13,2176,2177],{},"startrow=ws.max_row"," and ",[13,2180,1360],{}," when you genuinely want to add rows below existing data. Guard the first run, because ",[13,2183,23],{}," needs the file to exist, and guard against duplicates so a retry does not double a period. Then remember what it costs: every append rewrites the whole workbook, so when a script owns the file, accumulate the rows and write once instead.",[171,2186,2188],{"id":2187},"frequently-asked-questions","Frequently asked questions",[10,2190,2191,2194,2195,2197,2198,2201,2202,2204],{},[698,2192,2193],{},"Why does my existing sheet disappear when I write to the file?","\nThe default ",[13,2196,19],{}," mode is ",[13,2199,2200],{},"\"w\"",", which creates a new file and discards whatever was there. Pass ",[13,2203,23],{}," to open the existing workbook instead of replacing it.",[10,2206,2207,2210,2211,2213,2214,30],{},[698,2208,2209],{},"Can I append with the xlsxwriter engine?","\nNo. xlsxwriter can only create new files — it has no way to read an existing one. Appending requires ",[13,2212,1785],{},", which pandas selects automatically when ",[13,2215,23],{},[10,2217,2218,2225,2226,2228,2229,2231,2232,2234],{},[698,2219,2220,2221,2224],{},"What does ",[13,2222,2223],{},"if_sheet_exists=\"overlay\""," do?","\nIt writes into the existing sheet without removing it, so combined with ",[13,2227,888],{}," you can add rows below what is already there. The alternatives are ",[13,2230,610],{},", which discards the sheet's contents, and ",[13,2233,782],{},", which creates a numbered copy.",[10,2236,2237,2240,2241,2243],{},[698,2238,2239],{},"How do I find the row to append at?","\nOpen the workbook with openpyxl and read ",[13,2242,1339],{},". Be aware it reports the used range, so on a sheet with stray formatting it can overreport — scan up for the last non-empty row if that is a risk.",[10,2245,2246,2249],{},[698,2247,2248],{},"Is appending in a loop a good idea?","\nNo. Each append re-opens, parses and rewrites the whole workbook, so a loop is quadratic. Accumulate the rows in memory and write once, or append to CSV and convert at the end.",[171,2251,2253],{"id":2252},"related","Related",[2255,2256,2257,2264,2269,2276,2283],"ul",{},[2258,2259,2260,2261,2263],"li",{},"Up to the parent: ",[26,2262,29],{"href":28}," — the writing options this builds on.",[2258,2265,2266,2268],{},[26,2267,712],{"href":711}," — why only one engine can append.",[2258,2270,2271,2275],{},[26,2272,2274],{"href":2273},"\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"," — the cell-level equivalent.",[2258,2277,2278,2282],{},[26,2279,2281],{"href":2280},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fwrite-multiple-dataframes-to-one-excel-file\u002F","Write Multiple DataFrames to One Excel File"," — writing everything in a single pass instead.",[2258,2284,2285,2289],{},[26,2286,2288],{"href":2287},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python\u002F","Convert Excel to CSV with Python"," — the truly append-friendly format.",[2291,2292,2293],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":181,"searchDepth":231,"depth":231,"links":2295},[2296,2297,2298,2299,2300,2301,2302,2303,2304,2305],{"id":173,"depth":231,"text":174},{"id":362,"depth":231,"text":363},{"id":715,"depth":231,"text":716},{"id":892,"depth":231,"text":893},{"id":1523,"depth":231,"text":1524},{"id":1735,"depth":231,"text":1736},{"id":1901,"depth":231,"text":1902},{"id":2158,"depth":231,"text":2159},{"id":2187,"depth":231,"text":2188},{"id":2252,"depth":231,"text":2253},"2026-08-15","Add rows or a new sheet to a workbook that already exists — ExcelWriter mode 'a', if_sheet_exists options, appending below existing data, and the openpyxl-only limits.","md",[2310,2312,2314,2317,2319],{"q":2193,"a":2311},"The default ExcelWriter mode is 'w', which creates a new file and discards whatever was there. Pass mode=\"a\" to open the existing workbook instead of replacing it.",{"q":2209,"a":2313},"No. xlsxwriter can only create new files — it has no way to read an existing one. Appending requires engine=\"openpyxl\", which pandas selects automatically when mode=\"a\".",{"q":2315,"a":2316},"What does if_sheet_exists=\"overlay\" do?","It writes into the existing sheet without removing it, so combined with startrow you can add rows below what is already there. The alternatives are \"replace\", which discards the sheet's contents, and \"new\", which creates a numbered copy.",{"q":2239,"a":2318},"Open the workbook with openpyxl and read ws.max_row. Be aware it reports the used range, so on a sheet with stray formatting it can overreport — scan up for the last non-empty row if that is a risk.",{"q":2248,"a":2320},"No. Each append re-opens, parses and rewrites the whole workbook, so a loop is quadratic. Accumulate the rows in memory and write once, or append to CSV and convert at the end.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas",{"title":2324,"description":2325},"Append to an Existing Excel File with pandas","Use pd.ExcelWriter with mode='a' to add sheets or rows to an existing .xlsx — if_sheet_exists overlay, finding the last row, and why xlsxwriter cannot append.","append-a-dataframe-to-an-existing-excel-file-with-pandas","getting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fappend-a-dataframe-to-an-existing-excel-file-with-pandas\u002Findex","how-to","c_n7_LxRFUeG-d3RzV9AHLzeP0dU4cCIT6d_-TqWnko",[2331,2335],{"title":2332,"path":2333,"stem":2334,"children":-1},"Writing DataFrames to Excel with Pandas","\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas","getting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Findex",{"title":2336,"path":2337,"stem":2338,"children":-1},"Auto-Fit Column Widths When Writing with pandas","\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fauto-fit-column-widths-when-writing-with-pandas","getting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fauto-fit-column-widths-when-writing-with-pandas\u002Findex",1786800029158]