[{"data":1,"prerenderedAt":2091},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save":2055,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save":2064,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save":2069,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save":2070},{"id":4,"title":5,"body":6,"dateModified":2020,"datePublished":2020,"description":2021,"extension":2022,"faq":2023,"keyword":2032,"kind":2033,"meta":2034,"navigation":269,"path":2043,"seo":2044,"slug":2047,"stem":2048,"topics":2049,"type":2053,"__hash__":2054},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save\u002Findex.md","Fix \"ExcelWriter object has no attribute save\" in pandas",{"type":7,"value":8,"toc":2005},"minimark",[9,13,24,27,33,51,166,171,209,213,241,317,321,324,412,421,425,434,492,504,652,656,670,801,813,817,820,826,842,978,1007,1011,1020,1328,1337,1341,1347,1710,1724,1845,1849,1861,1865,1911,1915,1940,1944,1950,1956,1962,1968,1972,2001],[10,11,12],"p",{},"Scripts written before 2023 often end their Excel export like this:",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"language-text","writer = pd.ExcelWriter(\"report.xlsx\")\ndf.to_excel(writer, sheet_name=\"Data\")\nwriter.save()\n","text","",[22,23,18],"code",{"__ignoreMap":20},[10,25,26],{},"After upgrading pandas, the last line fails:",[14,28,31],{"className":29,"code":30,"language":19,"meta":20},[17],"AttributeError: 'OpenpyxlWriter' object has no attribute 'save'\n",[22,32,30],{"__ignoreMap":20},[10,34,35,36,39,40,44,45,50],{},"— or ",[22,37,38],{},"'XlsxWriter' object has no attribute 'save'",", depending on the engine. The method was deprecated in pandas 1.5 and removed in 2.0. Nothing is wrong with the rest of the code; the file is now written when the writer is ",[41,42,43],"em",{},"closed",". This guide, part of ",[46,47,49],"a",{"href":48},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",", shows the replacement and the related API changes that tend to break the same scripts.",[52,53,61,62,61,66,61,70,61,78,61,86,61,92,61,95,61,99,61,105,61,110,61,116,61,121,61,123,61,126,61,128,61,131,61,136,61,139,61,147,61,152,61,160],"svg",{"viewBox":54,"role":55,"ariaLabelledBy":56,"xmlns":59,"style":60},"34 0 691 140","img",[57,58],"save-fix-t","save-fix-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:691px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[63,64,65],"title",{"id":57},"Replacing writer.save() with a with block",[67,68,69],"desc",{"id":58},"The old pattern creates a writer, writes frames and calls save, which no longer exists. The new pattern opens the writer in a with block; leaving the block closes the writer and writes the file, even if an exception interrupts the code.",[71,72],"rect",{"x":73,"y":74,"width":75,"height":76,"fill":77},"34","0","691","140","#ffffff",[71,79],{"x":80,"y":81,"width":82,"height":83,"rx":74,"fill":77,"stroke":84,"style":85},"50.5","40.0","204.5","28.0","var(--line,#cdd5e6)","stroke-width:1px",[19,87,91],{"x":88,"y":89,"style":90},"152.8","59.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","writer = pd.ExcelWriter(path)",[71,93],{"x":80,"y":94,"width":82,"height":83,"rx":74,"fill":77,"stroke":84,"style":85},"68.0",[19,96,98],{"x":88,"y":97,"style":90},"87.0","df.to_excel(writer)",[71,100],{"x":80,"y":101,"width":82,"height":83,"rx":74,"fill":102,"stroke":103,"style":104},"96.0","#fee8f2","var(--accent,#f43f8f)","stroke-width:1.5px",[19,106,109],{"x":88,"y":107,"style":108},"115.0","font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","writer.save()",[71,111],{"x":112,"y":81,"width":113,"height":83,"rx":74,"fill":114,"stroke":115,"style":104},"465.4","244.1","#d9f4f1","var(--teal,#0f9488)",[19,117,120],{"x":118,"y":89,"style":119},"587.4","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","with pd.ExcelWriter(path) as writer:",[71,122],{"x":112,"y":94,"width":113,"height":83,"rx":74,"fill":77,"stroke":84,"style":85},[19,124,125],{"x":118,"y":97,"style":90},"    df.to_excel(writer)",[71,127],{"x":112,"y":101,"width":113,"height":83,"rx":74,"fill":77,"stroke":84,"style":85},[19,129,130],{"x":118,"y":107,"style":90},"# saved when the block ends",[19,132,135],{"x":88,"y":133,"style":134},"29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","pandas \u003C 2.0",[19,137,138],{"x":118,"y":133,"style":134},"pandas ≥ 2.0",[140,141],"line",{"x1":142,"y1":143,"x2":144,"y2":143,"stroke":145,"style":146},"265.0","82.0","451.4","var(--brand,#5b5cf0)","stroke-width:2px",[148,149],"polygon",{"points":150,"fill":151},"455.4,82.0 446.4,87.0 446.4,77.0","#5b5cf0",[71,153],{"x":154,"y":155,"width":156,"height":157,"rx":158,"fill":77,"stroke":84,"style":159},"292.0","49.0","136.3","22.0","11","stroke-width:1.2px",[19,161,165],{"x":162,"y":163,"style":164},"360.2","64.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","with … as writer",[167,168,170],"h2",{"id":169},"why-the-method-was-removed","Why the method was removed",[10,172,173,176,177,180,181,183,184,186,187,189,190,193,194,176,197,200,201,204,205,208],{},[22,174,175],{},"save()"," and ",[22,178,179],{},"close()"," had drifted into doing nearly the same job, and having both invited the pattern that caused real bugs: calling ",[22,182,175],{}," part-way, continuing to write, and never closing the file handle. pandas deprecated ",[22,185,175],{}," in 1.5 with a warning pointing at ",[22,188,179],{},", and removed it in 2.0 along with the ability to replace the writer's internal ",[22,191,192],{},"book",", ",[22,195,196],{},"sheets",[22,198,199],{},"handles"," — attributes that exposed engine internals and let callers put the writer into states pandas could not manage. The replacement is the standard Python idiom for anything that holds a file open: a context manager. If your scripts printed a ",[22,202,203],{},"FutureWarning"," mentioning ",[22,206,207],{},"save"," before the upgrade, that was the notice; running jobs with warnings visible in their logs is the cheapest way to see the next removal coming.",[167,210,212],{"id":211},"prerequisites","Prerequisites",[14,214,218],{"className":215,"code":216,"language":217,"meta":20,"style":20},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install \"pandas>=2\" openpyxl xlsxwriter\n","bash",[22,219,220],{"__ignoreMap":20},[221,222,224,228,232,235,238],"span",{"class":140,"line":223},1,[221,225,227],{"class":226},"sMTad","pip",[221,229,231],{"class":230},"srMev"," install",[221,233,234],{"class":230}," \"pandas>=2\"",[221,236,237],{"class":230}," openpyxl",[221,239,240],{"class":230}," xlsxwriter\n",[14,242,246],{"className":243,"code":244,"language":245,"meta":20,"style":20},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndf = pd.DataFrame({\"region\": [\"North\", \"South\"], \"sales\": [410, 388]})\n","python",[22,247,248,264,271],{"__ignoreMap":20},[221,249,250,254,258,261],{"class":140,"line":223},[221,251,253],{"class":252},"s-kum","import",[221,255,257],{"class":256},"skGVy"," pandas ",[221,259,260],{"class":252},"as",[221,262,263],{"class":256}," pd\n",[221,265,267],{"class":140,"line":266},2,[221,268,270],{"emptyLinePlaceholder":269},true,"\n",[221,272,274,277,280,283,286,289,292,294,297,300,303,305,309,311,314],{"class":140,"line":273},3,[221,275,276],{"class":256},"df ",[221,278,279],{"class":252},"=",[221,281,282],{"class":256}," pd.DataFrame({",[221,284,285],{"class":230},"\"region\"",[221,287,288],{"class":256},": [",[221,290,291],{"class":230},"\"North\"",[221,293,193],{"class":256},[221,295,296],{"class":230},"\"South\"",[221,298,299],{"class":256},"], ",[221,301,302],{"class":230},"\"sales\"",[221,304,288],{"class":256},[221,306,308],{"class":307},"sP0c6","410",[221,310,193],{"class":256},[221,312,313],{"class":307},"388",[221,315,316],{"class":256},"]})\n",[167,318,320],{"id":319},"step-1-use-a-with-block","Step 1 — Use a with block",[10,322,323],{},"The recommended form opens the writer as a context manager. Everything inside the block writes into the workbook, and leaving the block closes the writer, which saves the file:",[14,325,327],{"className":243,"code":326,"language":245,"meta":20,"style":20},"with pd.ExcelWriter(\"report.xlsx\") as writer:\n    df.to_excel(writer, sheet_name=\"Data\", index=False)\n    df.describe().to_excel(writer, sheet_name=\"Stats\")\n\nprint(pd.ExcelFile(\"report.xlsx\").sheet_names)   # ['Data', 'Stats']\n",[22,328,329,348,375,389,394],{"__ignoreMap":20},[221,330,331,334,337,340,343,345],{"class":140,"line":223},[221,332,333],{"class":252},"with",[221,335,336],{"class":256}," pd.ExcelWriter(",[221,338,339],{"class":230},"\"report.xlsx\"",[221,341,342],{"class":256},") ",[221,344,260],{"class":252},[221,346,347],{"class":256}," writer:\n",[221,349,350,353,357,359,362,364,367,369,372],{"class":140,"line":266},[221,351,352],{"class":256},"    df.to_excel(writer, ",[221,354,356],{"class":355},"sa561","sheet_name",[221,358,279],{"class":252},[221,360,361],{"class":230},"\"Data\"",[221,363,193],{"class":256},[221,365,366],{"class":355},"index",[221,368,279],{"class":252},[221,370,371],{"class":307},"False",[221,373,374],{"class":256},")\n",[221,376,377,380,382,384,387],{"class":140,"line":273},[221,378,379],{"class":256},"    df.describe().to_excel(writer, ",[221,381,356],{"class":355},[221,383,279],{"class":252},[221,385,386],{"class":230},"\"Stats\"",[221,388,374],{"class":256},[221,390,392],{"class":140,"line":391},4,[221,393,270],{"emptyLinePlaceholder":269},[221,395,397,400,403,405,408],{"class":140,"line":396},5,[221,398,399],{"class":307},"print",[221,401,402],{"class":256},"(pd.ExcelFile(",[221,404,339],{"class":230},[221,406,407],{"class":256},").sheet_names)   ",[221,409,411],{"class":410},"s-wDw","# ['Data', 'Stats']\n",[10,413,414,415,417,418,420],{},"The ",[22,416,333],{}," form also fixes a problem the old code had quietly: if an exception occurred between creating the writer and calling ",[22,419,175],{},", the file handle stayed open and the workbook was never written, or was left half-written. The context manager closes the writer however the block exits.",[167,422,424],{"id":423},"step-2-or-call-close","Step 2 — Or call close()",[10,426,427,428,430,431,433],{},"When the writer is created in one place and finished in another — a class that builds a report over several method calls, for instance — call ",[22,429,179],{}," where ",[22,432,175],{}," used to be:",[14,435,437],{"className":243,"code":436,"language":245,"meta":20,"style":20},"writer = pd.ExcelWriter(\"report_class.xlsx\", engine=\"openpyxl\")\ndf.to_excel(writer, sheet_name=\"Data\", index=False)\nwriter.close()          # was: writer.save()\n",[22,438,439,463,484],{"__ignoreMap":20},[221,440,441,444,446,448,451,453,456,458,461],{"class":140,"line":223},[221,442,443],{"class":256},"writer ",[221,445,279],{"class":252},[221,447,336],{"class":256},[221,449,450],{"class":230},"\"report_class.xlsx\"",[221,452,193],{"class":256},[221,454,455],{"class":355},"engine",[221,457,279],{"class":252},[221,459,460],{"class":230},"\"openpyxl\"",[221,462,374],{"class":256},[221,464,465,468,470,472,474,476,478,480,482],{"class":140,"line":266},[221,466,467],{"class":256},"df.to_excel(writer, ",[221,469,356],{"class":355},[221,471,279],{"class":252},[221,473,361],{"class":230},[221,475,193],{"class":256},[221,477,366],{"class":355},[221,479,279],{"class":252},[221,481,371],{"class":307},[221,483,374],{"class":256},[221,485,486,489],{"class":140,"line":273},[221,487,488],{"class":256},"writer.close()          ",[221,490,491],{"class":410},"# was: writer.save()\n",[10,493,494,495,497,498,500,501,503],{},"Treat ",[22,496,179],{}," as final. Old code sometimes called ",[22,499,175],{}," in the middle and kept writing; after ",[22,502,179],{}," the writer is finished, and writing again requires a new writer — in append mode if the file should keep what was already saved (Step 4).",[52,505,61,511,61,514,61,517,61,522,61,530,61,541,61,546,61,550,61,554,61,557,61,560,61,567,61,572,61,578,61,585,61,590,61,595,61,597,61,600,61,604,61,607,61,610,61,615,61,618,61,621,61,623,61,626,61,630,61,633,61,638,61,645,61,649],{"viewBox":506,"role":55,"ariaLabelledBy":507,"xmlns":59,"style":510},"4 0 752 272",[508,509],"save-which-t","save-which-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[63,512,513],{"id":508},"Choosing the replacement for writer.save()",[67,515,516],{"id":509},"If the writer is created and finished in one block of code, use a with statement. If it is created in one function and finished in another, call writer.close() at the end, ideally in a finally block. If the old code assigned writer.book to add sheets to an existing file, switch to append mode.",[71,518],{"x":519,"y":74,"width":520,"height":521,"fill":77},"4","752","272",[71,523],{"x":524,"y":525,"width":526,"height":527,"rx":528,"fill":529,"stroke":145,"style":146},"218.1","16.0","323.9","75.5","12","#f0f4ff",[531,532,534],"g",{"transform":533},"translate(368.0,29.0) scale(1.000)",[535,536],"path",{"d":537,"fill":538,"stroke":539,"style":540},"M9 8l-4 4 4 4 M15 8l4 4-4 4","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[19,542,545],{"x":543,"y":527,"style":544},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","How does the old code use the writer?",[140,547],{"x1":543,"y1":548,"x2":543,"y2":549,"stroke":145,"style":146},"91.5","111.5",[140,551],{"x1":552,"y1":549,"x2":553,"y2":549,"stroke":145,"style":146},"134.7","625.3",[140,555],{"x1":552,"y1":549,"x2":552,"y2":556,"stroke":145,"style":146},"157.5",[148,558],{"points":559,"fill":151},"134.7,161.5 129.7,152.5 139.7,152.5",[71,561],{"x":562,"y":563,"width":564,"height":565,"rx":566,"fill":77,"stroke":84,"style":159},"97.0","121.5","75.4","21.0","10",[19,568,571],{"x":552,"y":569,"style":570},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","one block",[71,573],{"x":574,"y":575,"width":576,"height":577,"rx":528,"fill":114,"stroke":115,"style":146},"20.0","163.5","229.3","93.0",[531,579,581],{"transform":580},"translate(122.7,176.5) scale(1.000)",[535,582],{"d":583,"fill":538,"stroke":584,"style":540},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[19,586,589],{"x":552,"y":587,"style":588},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","with statement",[19,591,594],{"x":552,"y":592,"style":593},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","saves on exit",[140,596],{"x1":543,"y1":549,"x2":543,"y2":556,"stroke":145,"style":146},[148,598],{"points":599,"fill":151},"380.0,161.5 375.0,152.5 385.0,152.5",[71,601],{"x":602,"y":563,"width":603,"height":565,"rx":566,"fill":77,"stroke":84,"style":159},"321.4","117.1",[19,605,606],{"x":543,"y":569,"style":570},"across functions",[71,608],{"x":609,"y":575,"width":576,"height":577,"rx":528,"fill":529,"stroke":145,"style":146},"265.3",[531,611,613],{"transform":612},"translate(368.0,176.5) scale(1.000)",[535,614],{"d":537,"fill":538,"stroke":539,"style":540},[19,616,617],{"x":543,"y":587,"style":544},"writer.close()",[19,619,620],{"x":543,"y":592,"style":593},"in a finally block",[140,622],{"x1":553,"y1":549,"x2":553,"y2":556,"stroke":145,"style":146},[148,624],{"points":625,"fill":151},"625.3,161.5 620.3,152.5 630.3,152.5",[71,627],{"x":628,"y":563,"width":629,"height":565,"rx":566,"fill":77,"stroke":84,"style":159},"530.1","190.4",[19,631,632],{"x":553,"y":569,"style":570},"writer.book = load_workbook",[71,634],{"x":635,"y":575,"width":576,"height":577,"rx":528,"fill":636,"stroke":637,"style":146},"510.7","#fdefd8","var(--gold,#b4740a)",[531,639,641],{"transform":640},"translate(613.3,176.5) scale(1.000)",[535,642],{"d":643,"fill":538,"stroke":644,"style":540},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--gold-ink,#7a4e06)",[19,646,648],{"x":553,"y":587,"style":647},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","mode=\"a\"",[19,650,651],{"x":553,"y":592,"style":593},"pandas loads the file",[167,653,655],{"id":654},"step-3-keep-formatting-code-that-uses-the-engine","Step 3 — Keep formatting code that uses the engine",[10,657,658,659,176,662,665,666,669],{},"Many exports format the sheets through the engine after writing — column widths, number formats, frozen panes. That still works: ",[22,660,661],{},"writer.book",[22,663,664],{},"writer.sheets"," are available ",[41,667,668],{},"inside"," the block, so move the formatting code in with the writes:",[14,671,673],{"className":243,"code":672,"language":245,"meta":20,"style":20},"with pd.ExcelWriter(\"formatted.xlsx\", engine=\"xlsxwriter\") as writer:\n    df.to_excel(writer, sheet_name=\"Data\", index=False)\n    book, sheet = writer.book, writer.sheets[\"Data\"]\n    money = book.add_format({\"num_format\": \"#,##0\"})\n    sheet.set_column(\"A:A\", 14)\n    sheet.set_column(\"B:B\", 12, money)\n    sheet.freeze_panes(1, 0)\n",[22,674,675,699,719,734,756,771,786],{"__ignoreMap":20},[221,676,677,679,681,684,686,688,690,693,695,697],{"class":140,"line":223},[221,678,333],{"class":252},[221,680,336],{"class":256},[221,682,683],{"class":230},"\"formatted.xlsx\"",[221,685,193],{"class":256},[221,687,455],{"class":355},[221,689,279],{"class":252},[221,691,692],{"class":230},"\"xlsxwriter\"",[221,694,342],{"class":256},[221,696,260],{"class":252},[221,698,347],{"class":256},[221,700,701,703,705,707,709,711,713,715,717],{"class":140,"line":266},[221,702,352],{"class":256},[221,704,356],{"class":355},[221,706,279],{"class":252},[221,708,361],{"class":230},[221,710,193],{"class":256},[221,712,366],{"class":355},[221,714,279],{"class":252},[221,716,371],{"class":307},[221,718,374],{"class":256},[221,720,721,724,726,729,731],{"class":140,"line":273},[221,722,723],{"class":256},"    book, sheet ",[221,725,279],{"class":252},[221,727,728],{"class":256}," writer.book, writer.sheets[",[221,730,361],{"class":230},[221,732,733],{"class":256},"]\n",[221,735,736,739,741,744,747,750,753],{"class":140,"line":391},[221,737,738],{"class":256},"    money ",[221,740,279],{"class":252},[221,742,743],{"class":256}," book.add_format({",[221,745,746],{"class":230},"\"num_format\"",[221,748,749],{"class":256},": ",[221,751,752],{"class":230},"\"#,##0\"",[221,754,755],{"class":256},"})\n",[221,757,758,761,764,766,769],{"class":140,"line":396},[221,759,760],{"class":256},"    sheet.set_column(",[221,762,763],{"class":230},"\"A:A\"",[221,765,193],{"class":256},[221,767,768],{"class":307},"14",[221,770,374],{"class":256},[221,772,774,776,779,781,783],{"class":140,"line":773},6,[221,775,760],{"class":256},[221,777,778],{"class":230},"\"B:B\"",[221,780,193],{"class":256},[221,782,528],{"class":307},[221,784,785],{"class":256},", money)\n",[221,787,789,792,795,797,799],{"class":140,"line":788},7,[221,790,791],{"class":256},"    sheet.freeze_panes(",[221,793,794],{"class":307},"1",[221,796,193],{"class":256},[221,798,74],{"class":307},[221,800,374],{"class":256},[10,802,803,804,176,806,808,809,812],{},"Reading ",[22,805,661],{},[22,807,664],{}," is fine. What changed is ",[41,810,811],{},"assigning"," to them, which is the next fix.",[167,814,816],{"id":815},"step-4-replace-writerbook-assignment-with-append-mode","Step 4 — Replace writer.book assignment with append mode",[10,818,819],{},"The other removed pattern appended to an existing workbook by loading it with openpyxl and handing it to the writer:",[14,821,824],{"className":822,"code":823,"language":19,"meta":20},[17],"book = load_workbook(\"report.xlsx\")\nwriter = pd.ExcelWriter(\"report.xlsx\", engine=\"openpyxl\")\nwriter.book = book\nwriter.sheets = {ws.title: ws for ws in book.worksheets}\n",[22,825,823],{"__ignoreMap":20},[10,827,828,829,176,831,833,834,837,838,841],{},"In pandas 2 both assignments fail — ",[22,830,192],{},[22,832,196],{}," are read-only properties, and the first line raises ",[22,835,836],{},"AttributeError: property 'book' of 'OpenpyxlWriter' object has no setter",". Append mode does the loading for you, and ",[22,839,840],{},"if_sheet_exists"," states what should happen when a sheet name is already taken:",[14,843,845],{"className":243,"code":844,"language":245,"meta":20,"style":20},"extra = pd.DataFrame({\"region\": [\"East\"], \"sales\": [295]})\nwith pd.ExcelWriter(\"report.xlsx\", mode=\"a\", engine=\"openpyxl\", if_sheet_exists=\"replace\") as writer:\n    extra.to_excel(writer, sheet_name=\"Latest\", index=False)\n    df.to_excel(writer, sheet_name=\"Data\", index=False)          # replaces the old Data sheet\n\nprint(pd.ExcelFile(\"report.xlsx\").sheet_names)   # ['Data', 'Stats', 'Latest']\n",[22,846,847,874,915,937,961,965],{"__ignoreMap":20},[221,848,849,852,854,856,858,860,863,865,867,869,872],{"class":140,"line":223},[221,850,851],{"class":256},"extra ",[221,853,279],{"class":252},[221,855,282],{"class":256},[221,857,285],{"class":230},[221,859,288],{"class":256},[221,861,862],{"class":230},"\"East\"",[221,864,299],{"class":256},[221,866,302],{"class":230},[221,868,288],{"class":256},[221,870,871],{"class":307},"295",[221,873,316],{"class":256},[221,875,876,878,880,882,884,887,889,892,894,896,898,900,902,904,906,909,911,913],{"class":140,"line":266},[221,877,333],{"class":252},[221,879,336],{"class":256},[221,881,339],{"class":230},[221,883,193],{"class":256},[221,885,886],{"class":355},"mode",[221,888,279],{"class":252},[221,890,891],{"class":230},"\"a\"",[221,893,193],{"class":256},[221,895,455],{"class":355},[221,897,279],{"class":252},[221,899,460],{"class":230},[221,901,193],{"class":256},[221,903,840],{"class":355},[221,905,279],{"class":252},[221,907,908],{"class":230},"\"replace\"",[221,910,342],{"class":256},[221,912,260],{"class":252},[221,914,347],{"class":256},[221,916,917,920,922,924,927,929,931,933,935],{"class":140,"line":273},[221,918,919],{"class":256},"    extra.to_excel(writer, ",[221,921,356],{"class":355},[221,923,279],{"class":252},[221,925,926],{"class":230},"\"Latest\"",[221,928,193],{"class":256},[221,930,366],{"class":355},[221,932,279],{"class":252},[221,934,371],{"class":307},[221,936,374],{"class":256},[221,938,939,941,943,945,947,949,951,953,955,958],{"class":140,"line":391},[221,940,352],{"class":256},[221,942,356],{"class":355},[221,944,279],{"class":252},[221,946,361],{"class":230},[221,948,193],{"class":256},[221,950,366],{"class":355},[221,952,279],{"class":252},[221,954,371],{"class":307},[221,956,957],{"class":256},")          ",[221,959,960],{"class":410},"# replaces the old Data sheet\n",[221,962,963],{"class":140,"line":396},[221,964,270],{"emptyLinePlaceholder":269},[221,966,967,969,971,973,975],{"class":140,"line":773},[221,968,399],{"class":307},[221,970,402],{"class":256},[221,972,339],{"class":230},[221,974,407],{"class":256},[221,976,977],{"class":410},"# ['Data', 'Stats', 'Latest']\n",[10,979,980,981,983,984,193,987,193,990,176,993,996,997,1001,1002,1006],{},"Append mode needs the openpyxl engine and an existing file. The options for ",[22,982,840],{}," — ",[22,985,986],{},"error",[22,988,989],{},"new",[22,991,992],{},"replace",[22,994,995],{},"overlay"," — are compared in ",[46,998,1000],{"href":999},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fwrite-a-dataframe-to-a-specific-cell-in-excel\u002F","Write a DataFrame to a Specific Cell in Excel with pandas",", and appending rows to an existing sheet in ",[46,1003,1005],{"href":1004},"\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",".",[167,1008,1010],{"id":1009},"step-5-migrate-a-writer-shared-across-a-report-class","Step 5 — Migrate a writer shared across a report class",[10,1012,1013,1014,1016,1017,1019],{},"Larger scripts often wrap the writer in a class: one method opens it, others add sheets, and a final method used to call ",[22,1015,175],{},". The clean migration makes the class itself a context manager, so callers get the same guarantee as a ",[22,1018,333],{}," block:",[14,1021,1023],{"className":243,"code":1022,"language":245,"meta":20,"style":20},"class RegionReport:\n    def __init__(self, path: str):\n        self.path = path\n        self.writer = None\n\n    def __enter__(self):\n        self.writer = pd.ExcelWriter(self.path, engine=\"xlsxwriter\")\n        return self\n\n    def add(self, name: str, frame: pd.DataFrame) -> None:\n        frame.to_excel(self.writer, sheet_name=name, index=False)\n        self.writer.sheets[name].set_column(0, len(frame.columns) - 1, 14)\n\n    def __exit__(self, exc_type, exc, tb):\n        self.writer.close()          # was: self.writer.save()\n        return False                 # let exceptions propagate\n\nwith RegionReport(\"regions.xlsx\") as report:\n    report.add(\"North\", df[df[\"region\"] == \"North\"])\n    report.add(\"South\", df[df[\"region\"] == \"South\"])\nprint(pd.ExcelFile(\"regions.xlsx\").sheet_names)   # ['North', 'South']\n",[22,1024,1025,1036,1053,1066,1078,1082,1092,1116,1125,1130,1152,1178,1208,1213,1224,1235,1246,1251,1269,1294,1314],{"__ignoreMap":20},[221,1026,1027,1030,1033],{"class":140,"line":223},[221,1028,1029],{"class":252},"class",[221,1031,1032],{"class":226}," RegionReport",[221,1034,1035],{"class":256},":\n",[221,1037,1038,1041,1044,1047,1050],{"class":140,"line":266},[221,1039,1040],{"class":252},"    def",[221,1042,1043],{"class":307}," __init__",[221,1045,1046],{"class":256},"(self, path: ",[221,1048,1049],{"class":307},"str",[221,1051,1052],{"class":256},"):\n",[221,1054,1055,1058,1061,1063],{"class":140,"line":273},[221,1056,1057],{"class":307},"        self",[221,1059,1060],{"class":256},".path ",[221,1062,279],{"class":252},[221,1064,1065],{"class":256}," path\n",[221,1067,1068,1070,1073,1075],{"class":140,"line":391},[221,1069,1057],{"class":307},[221,1071,1072],{"class":256},".writer ",[221,1074,279],{"class":252},[221,1076,1077],{"class":307}," None\n",[221,1079,1080],{"class":140,"line":396},[221,1081,270],{"emptyLinePlaceholder":269},[221,1083,1084,1086,1089],{"class":140,"line":773},[221,1085,1040],{"class":252},[221,1087,1088],{"class":307}," __enter__",[221,1090,1091],{"class":256},"(self):\n",[221,1093,1094,1096,1098,1100,1102,1105,1108,1110,1112,1114],{"class":140,"line":788},[221,1095,1057],{"class":307},[221,1097,1072],{"class":256},[221,1099,279],{"class":252},[221,1101,336],{"class":256},[221,1103,1104],{"class":307},"self",[221,1106,1107],{"class":256},".path, ",[221,1109,455],{"class":355},[221,1111,279],{"class":252},[221,1113,692],{"class":230},[221,1115,374],{"class":256},[221,1117,1119,1122],{"class":140,"line":1118},8,[221,1120,1121],{"class":252},"        return",[221,1123,1124],{"class":307}," self\n",[221,1126,1128],{"class":140,"line":1127},9,[221,1129,270],{"emptyLinePlaceholder":269},[221,1131,1133,1135,1139,1142,1144,1147,1150],{"class":140,"line":1132},10,[221,1134,1040],{"class":252},[221,1136,1138],{"class":1137},"s_Opv"," add",[221,1140,1141],{"class":256},"(self, name: ",[221,1143,1049],{"class":307},[221,1145,1146],{"class":256},", frame: pd.DataFrame) -> ",[221,1148,1149],{"class":307},"None",[221,1151,1035],{"class":256},[221,1153,1155,1158,1160,1163,1165,1167,1170,1172,1174,1176],{"class":140,"line":1154},11,[221,1156,1157],{"class":256},"        frame.to_excel(",[221,1159,1104],{"class":307},[221,1161,1162],{"class":256},".writer, ",[221,1164,356],{"class":355},[221,1166,279],{"class":252},[221,1168,1169],{"class":256},"name, ",[221,1171,366],{"class":355},[221,1173,279],{"class":252},[221,1175,371],{"class":307},[221,1177,374],{"class":256},[221,1179,1181,1183,1186,1188,1190,1193,1196,1199,1202,1204,1206],{"class":140,"line":1180},12,[221,1182,1057],{"class":307},[221,1184,1185],{"class":256},".writer.sheets[name].set_column(",[221,1187,74],{"class":307},[221,1189,193],{"class":256},[221,1191,1192],{"class":307},"len",[221,1194,1195],{"class":256},"(frame.columns) ",[221,1197,1198],{"class":252},"-",[221,1200,1201],{"class":307}," 1",[221,1203,193],{"class":256},[221,1205,768],{"class":307},[221,1207,374],{"class":256},[221,1209,1211],{"class":140,"line":1210},13,[221,1212,270],{"emptyLinePlaceholder":269},[221,1214,1216,1218,1221],{"class":140,"line":1215},14,[221,1217,1040],{"class":252},[221,1219,1220],{"class":307}," __exit__",[221,1222,1223],{"class":256},"(self, exc_type, exc, tb):\n",[221,1225,1227,1229,1232],{"class":140,"line":1226},15,[221,1228,1057],{"class":307},[221,1230,1231],{"class":256},".writer.close()          ",[221,1233,1234],{"class":410},"# was: self.writer.save()\n",[221,1236,1238,1240,1243],{"class":140,"line":1237},16,[221,1239,1121],{"class":252},[221,1241,1242],{"class":307}," False",[221,1244,1245],{"class":410},"                 # let exceptions propagate\n",[221,1247,1249],{"class":140,"line":1248},17,[221,1250,270],{"emptyLinePlaceholder":269},[221,1252,1254,1256,1259,1262,1264,1266],{"class":140,"line":1253},18,[221,1255,333],{"class":252},[221,1257,1258],{"class":256}," RegionReport(",[221,1260,1261],{"class":230},"\"regions.xlsx\"",[221,1263,342],{"class":256},[221,1265,260],{"class":252},[221,1267,1268],{"class":256}," report:\n",[221,1270,1272,1275,1277,1280,1282,1285,1288,1291],{"class":140,"line":1271},19,[221,1273,1274],{"class":256},"    report.add(",[221,1276,291],{"class":230},[221,1278,1279],{"class":256},", df[df[",[221,1281,285],{"class":230},[221,1283,1284],{"class":256},"] ",[221,1286,1287],{"class":252},"==",[221,1289,1290],{"class":230}," \"North\"",[221,1292,1293],{"class":256},"])\n",[221,1295,1297,1299,1301,1303,1305,1307,1309,1312],{"class":140,"line":1296},20,[221,1298,1274],{"class":256},[221,1300,296],{"class":230},[221,1302,1279],{"class":256},[221,1304,285],{"class":230},[221,1306,1284],{"class":256},[221,1308,1287],{"class":252},[221,1310,1311],{"class":230}," \"South\"",[221,1313,1293],{"class":256},[221,1315,1317,1319,1321,1323,1325],{"class":140,"line":1316},21,[221,1318,399],{"class":307},[221,1320,402],{"class":256},[221,1322,1261],{"class":230},[221,1324,407],{"class":256},[221,1326,1327],{"class":410},"# ['North', 'South']\n",[10,1329,1330,1333,1334,1336],{},[22,1331,1332],{},"__exit__"," runs whether the block finishes normally or raises, so the writer is always closed and the file handle released. Returning ",[22,1335,371],{}," means errors still surface to the caller — the report is saved as far as it got, and the job fails visibly rather than silently.",[167,1338,1340],{"id":1339},"step-6-find-every-old-call-in-a-codebase","Step 6 — Find every old call in a codebase",[10,1342,1343,1344,1346],{},"A script that fails on ",[22,1345,175],{}," usually has siblings. Search the whole project once and fix them together, rather than one failed job at a time:",[14,1348,1350],{"className":243,"code":1349,"language":245,"meta":20,"style":20},"import re\nfrom pathlib import Path\n\nOLD_API = {\n    r\"\\.save\\(\\s*\\)\": \"writer.save() -> with block or writer.close()\",\n    r\"\\.book\\s*=(?!=)\": \"writer.book = ... -> ExcelWriter(mode='a')\",\n    r\"\\.sheets\\s*=(?!=)\": \"writer.sheets = ... -> ExcelWriter(mode='a')\",\n    r\"\\.handles\\b\": \"writer.handles was removed\",\n}\n\nPath(\"legacy_job.py\").write_text(\"w = pd.ExcelWriter('x.xlsx')\\nw.book = wb\\nw.save()\\n\")\n\nfor path in Path(\".\").rglob(\"*.py\"):\n    text = path.read_text(encoding=\"utf-8\", errors=\"ignore\")\n    for pattern, advice in OLD_API.items():\n        for m in re.finditer(pattern, text):\n            line = text.count(\"\\n\", 0, m.start()) + 1\n            print(f\"{path}:{line}: {advice}\")\n",[22,1351,1352,1359,1372,1376,1387,1423,1456,1487,1509,1514,1518,1550,1554,1579,1609,1625,1638,1667],{"__ignoreMap":20},[221,1353,1354,1356],{"class":140,"line":223},[221,1355,253],{"class":252},[221,1357,1358],{"class":256}," re\n",[221,1360,1361,1364,1367,1369],{"class":140,"line":266},[221,1362,1363],{"class":252},"from",[221,1365,1366],{"class":256}," pathlib ",[221,1368,253],{"class":252},[221,1370,1371],{"class":256}," Path\n",[221,1373,1374],{"class":140,"line":273},[221,1375,270],{"emptyLinePlaceholder":269},[221,1377,1378,1381,1384],{"class":140,"line":391},[221,1379,1380],{"class":307},"OLD_API",[221,1382,1383],{"class":252}," =",[221,1385,1386],{"class":256}," {\n",[221,1388,1389,1392,1395,1399,1401,1404,1407,1410,1413,1415,1417,1420],{"class":140,"line":396},[221,1390,1391],{"class":252},"    r",[221,1393,1394],{"class":230},"\"",[221,1396,1398],{"class":1397},"s_b0D","\\.",[221,1400,207],{"class":230},[221,1402,1403],{"class":1397},"\\(",[221,1405,1406],{"class":307},"\\s",[221,1408,1409],{"class":252},"*",[221,1411,1412],{"class":1397},"\\)",[221,1414,1394],{"class":230},[221,1416,749],{"class":256},[221,1418,1419],{"class":230},"\"writer.save() -> with block or writer.close()\"",[221,1421,1422],{"class":256},",\n",[221,1424,1425,1427,1429,1431,1433,1435,1437,1439,1442,1444,1447,1449,1451,1454],{"class":140,"line":773},[221,1426,1391],{"class":252},[221,1428,1394],{"class":230},[221,1430,1398],{"class":1397},[221,1432,192],{"class":230},[221,1434,1406],{"class":307},[221,1436,1409],{"class":252},[221,1438,279],{"class":230},[221,1440,1441],{"class":252},"(?!",[221,1443,279],{"class":230},[221,1445,1446],{"class":252},")",[221,1448,1394],{"class":230},[221,1450,749],{"class":256},[221,1452,1453],{"class":230},"\"writer.book = ... -> ExcelWriter(mode='a')\"",[221,1455,1422],{"class":256},[221,1457,1458,1460,1462,1464,1466,1468,1470,1472,1474,1476,1478,1480,1482,1485],{"class":140,"line":788},[221,1459,1391],{"class":252},[221,1461,1394],{"class":230},[221,1463,1398],{"class":1397},[221,1465,196],{"class":230},[221,1467,1406],{"class":307},[221,1469,1409],{"class":252},[221,1471,279],{"class":230},[221,1473,1441],{"class":252},[221,1475,279],{"class":230},[221,1477,1446],{"class":252},[221,1479,1394],{"class":230},[221,1481,749],{"class":256},[221,1483,1484],{"class":230},"\"writer.sheets = ... -> ExcelWriter(mode='a')\"",[221,1486,1422],{"class":256},[221,1488,1489,1491,1493,1495,1497,1500,1502,1504,1507],{"class":140,"line":1118},[221,1490,1391],{"class":252},[221,1492,1394],{"class":230},[221,1494,1398],{"class":1397},[221,1496,199],{"class":230},[221,1498,1499],{"class":307},"\\b",[221,1501,1394],{"class":230},[221,1503,749],{"class":256},[221,1505,1506],{"class":230},"\"writer.handles was removed\"",[221,1508,1422],{"class":256},[221,1510,1511],{"class":140,"line":1127},[221,1512,1513],{"class":256},"}\n",[221,1515,1516],{"class":140,"line":1132},[221,1517,270],{"emptyLinePlaceholder":269},[221,1519,1520,1523,1526,1529,1532,1536,1539,1541,1544,1546,1548],{"class":140,"line":1154},[221,1521,1522],{"class":256},"Path(",[221,1524,1525],{"class":230},"\"legacy_job.py\"",[221,1527,1528],{"class":256},").write_text(",[221,1530,1531],{"class":230},"\"w = pd.ExcelWriter('x.xlsx')",[221,1533,1535],{"class":1534},"sSjpA","\\n",[221,1537,1538],{"class":230},"w.book = wb",[221,1540,1535],{"class":1534},[221,1542,1543],{"class":230},"w.save()",[221,1545,1535],{"class":1534},[221,1547,1394],{"class":230},[221,1549,374],{"class":256},[221,1551,1552],{"class":140,"line":1180},[221,1553,270],{"emptyLinePlaceholder":269},[221,1555,1556,1559,1562,1565,1568,1571,1574,1577],{"class":140,"line":1210},[221,1557,1558],{"class":252},"for",[221,1560,1561],{"class":256}," path ",[221,1563,1564],{"class":252},"in",[221,1566,1567],{"class":256}," Path(",[221,1569,1570],{"class":230},"\".\"",[221,1572,1573],{"class":256},").rglob(",[221,1575,1576],{"class":230},"\"*.py\"",[221,1578,1052],{"class":256},[221,1580,1581,1584,1586,1589,1592,1594,1597,1599,1602,1604,1607],{"class":140,"line":1215},[221,1582,1583],{"class":256},"    text ",[221,1585,279],{"class":252},[221,1587,1588],{"class":256}," path.read_text(",[221,1590,1591],{"class":355},"encoding",[221,1593,279],{"class":252},[221,1595,1596],{"class":230},"\"utf-8\"",[221,1598,193],{"class":256},[221,1600,1601],{"class":355},"errors",[221,1603,279],{"class":252},[221,1605,1606],{"class":230},"\"ignore\"",[221,1608,374],{"class":256},[221,1610,1611,1614,1617,1619,1622],{"class":140,"line":1226},[221,1612,1613],{"class":252},"    for",[221,1615,1616],{"class":256}," pattern, advice ",[221,1618,1564],{"class":252},[221,1620,1621],{"class":307}," OLD_API",[221,1623,1624],{"class":256},".items():\n",[221,1626,1627,1630,1633,1635],{"class":140,"line":1237},[221,1628,1629],{"class":252},"        for",[221,1631,1632],{"class":256}," m ",[221,1634,1564],{"class":252},[221,1636,1637],{"class":256}," re.finditer(pattern, text):\n",[221,1639,1640,1643,1645,1648,1650,1652,1654,1656,1658,1661,1664],{"class":140,"line":1248},[221,1641,1642],{"class":256},"            line ",[221,1644,279],{"class":252},[221,1646,1647],{"class":256}," text.count(",[221,1649,1394],{"class":230},[221,1651,1535],{"class":1534},[221,1653,1394],{"class":230},[221,1655,193],{"class":256},[221,1657,74],{"class":307},[221,1659,1660],{"class":256},", m.start()) ",[221,1662,1663],{"class":252},"+",[221,1665,1666],{"class":307}," 1\n",[221,1668,1669,1672,1675,1678,1680,1683,1685,1688,1691,1693,1695,1697,1699,1701,1704,1706,1708],{"class":140,"line":1253},[221,1670,1671],{"class":307},"            print",[221,1673,1674],{"class":256},"(",[221,1676,1677],{"class":252},"f",[221,1679,1394],{"class":230},[221,1681,1682],{"class":1534},"{",[221,1684,535],{"class":256},[221,1686,1687],{"class":1534},"}",[221,1689,1690],{"class":230},":",[221,1692,1682],{"class":1534},[221,1694,140],{"class":256},[221,1696,1687],{"class":1534},[221,1698,749],{"class":230},[221,1700,1682],{"class":1534},[221,1702,1703],{"class":256},"advice",[221,1705,1687],{"class":1534},[221,1707,1394],{"class":230},[221,1709,374],{"class":256},[10,1711,414,1712,1715,1716,1719,1720,1723],{},[22,1713,1714],{},".save()"," pattern also matches ",[22,1717,1718],{},"wb.save()"," on openpyxl workbooks, which is still correct — openpyxl's own ",[22,1721,1722],{},"save(path)"," takes a filename, so calls with an argument are skipped by the pattern, but review each hit rather than replacing blindly.",[52,1725,61,1730,61,1733,61,1736,61,1739,61,1742,61,1746,61,1751,61,1754,61,1758,61,1763,61,1768,61,1770,61,1774,61,1776,61,1779,61,1782,61,1786,61,1788,61,1791,61,1793,61,1796,61,1799,61,1803,61,1805,61,1808,61,1810,61,1813,61,1816,61,1820,61,1822,61,1825,61,1827,61,1829,61,1832,61,1836,61,1838,61,1841,61,1843],{"viewBox":1726,"role":55,"ariaLabelledBy":1727,"xmlns":59,"style":510},"4 0 752 242",[1728,1729],"save-api-t","save-api-d",[63,1731,1732],{"id":1728},"ExcelWriter methods and attributes that changed in pandas 2",[67,1734,1735],{"id":1729},"The save method was removed in favour of close or a with block. Assigning book and sheets is no longer allowed, replaced by append mode. Reading book and sheets still works. The handles attribute was removed.",[71,1737],{"x":519,"y":74,"width":520,"height":1738,"fill":77},"242",[71,1740],{"x":574,"y":525,"width":1741,"height":81,"rx":74,"fill":77,"stroke":84,"style":85},"213.6",[71,1743],{"x":1744,"y":525,"width":1745,"height":81,"rx":74,"fill":529,"stroke":145,"style":159},"233.6","253.2",[19,1747,1750],{"x":162,"y":1748,"style":1749},"41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pandas 1.x",[71,1752],{"x":1753,"y":525,"width":1745,"height":81,"rx":74,"fill":529,"stroke":145,"style":159},"486.8",[19,1755,1757],{"x":1756,"y":1748,"style":1749},"613.4","pandas 2 and later",[71,1759],{"x":574,"y":1760,"width":1741,"height":1761,"rx":74,"fill":1762,"stroke":84,"style":85},"56.0","34.0","#e7ebef",[19,1764,109],{"x":1765,"y":1766,"style":1767},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[71,1769],{"x":1744,"y":1760,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1771,1773],{"x":162,"y":1772,"style":593},"77.0","works",[71,1775],{"x":1753,"y":1760,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1777,1778],{"x":1756,"y":1772,"style":593},"removed — use close()",[71,1780],{"x":574,"y":1781,"width":1741,"height":1761,"rx":74,"fill":1762,"stroke":84,"style":85},"90.0",[19,1783,1785],{"x":1765,"y":1784,"style":1767},"112.0","with ExcelWriter(...)",[71,1787],{"x":1744,"y":1781,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1789,1773],{"x":162,"y":1790,"style":593},"111.0",[71,1792],{"x":1753,"y":1781,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1794,1795],{"x":1756,"y":1790,"style":593},"works — preferred",[71,1797],{"x":574,"y":1798,"width":1741,"height":1761,"rx":74,"fill":1762,"stroke":84,"style":85},"124.0",[19,1800,1802],{"x":1765,"y":1801,"style":1767},"146.0","writer.book = wb",[71,1804],{"x":1744,"y":1798,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1806,1773],{"x":162,"y":1807,"style":593},"145.0",[71,1809],{"x":1753,"y":1798,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1811,1812],{"x":1756,"y":1807,"style":593},"removed — mode=\"a\"",[71,1814],{"x":574,"y":1815,"width":1741,"height":1761,"rx":74,"fill":1762,"stroke":84,"style":85},"158.0",[19,1817,1819],{"x":1765,"y":1818,"style":1767},"180.0","writer.sheets = {...}",[71,1821],{"x":1744,"y":1815,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1823,1773],{"x":162,"y":1824,"style":593},"179.0",[71,1826],{"x":1753,"y":1815,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1828,1812],{"x":1756,"y":1824,"style":593},[71,1830],{"x":574,"y":1831,"width":1741,"height":1761,"rx":74,"fill":1762,"stroke":84,"style":85},"192.0",[19,1833,1835],{"x":1765,"y":1834,"style":1767},"214.0","reading writer.book \u002F .sheets",[71,1837],{"x":1744,"y":1831,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1839,1773],{"x":162,"y":1840,"style":593},"213.0",[71,1842],{"x":1753,"y":1831,"width":1745,"height":1761,"rx":74,"fill":77,"stroke":84,"style":85},[19,1844,1773],{"x":1756,"y":1840,"style":593},[167,1846,1848],{"id":1847},"pin-versions-so-the-next-upgrade-is-deliberate","Pin versions so the next upgrade is deliberate",[10,1850,1851,1852,1855,1856,1860],{},"The error appeared because pandas was upgraded underneath working code — usually by an unpinned ",[22,1853,1854],{},"pip install pandas"," on a new machine or in a rebuilt container. Pin the versions a job was tested with in its requirements file, and upgrade them on purpose, with the job's tests run against the new versions first. A small smoke test that writes and reads back a workbook, as in ",[46,1857,1859],{"href":1858},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest",", catches this class of break before the scheduled run does.",[167,1862,1864],{"id":1863},"common-pitfalls","Common pitfalls",[1866,1867,1868,1878,1884,1893,1902],"ul",{},[1869,1870,1871,1875,1876,1006],"li",{},[1872,1873,1874],"strong",{},"Calling close() twice."," Closing an already-closed writer is harmless, but writing after close is not; restructure code that wrote after the old ",[22,1877,175],{},[1869,1879,1880,1883],{},[1872,1881,1882],{},"Mixing a with block and an explicit close()."," Unnecessary — the block closes the writer.",[1869,1885,1886,1889,1890,1892],{},[1872,1887,1888],{},"Append mode with xlsxwriter."," xlsxwriter cannot open existing files; pandas raises if you combine ",[22,1891,648],{}," with it.",[1869,1894,1895,1898,1899,1901],{},[1872,1896,1897],{},"Notebook cells that never close."," In Jupyter, a writer created in one cell and never closed keeps the file locked and unwritten. Use a ",[22,1900,333],{}," block inside a single cell.",[1869,1903,1904,1907,1908,1910],{},[1872,1905,1906],{},"Assuming the old code saved on exceptions."," It often did not. The ",[22,1909,333],{}," form is not just a rename; it is more reliable than the code it replaces.",[167,1912,1914],{"id":1913},"conclusion","Conclusion",[10,1916,1917,1920,1921,1924,1925,1927,1928,176,1931,1934,1935,176,1937,1939],{},[22,1918,1919],{},"ExcelWriter.save()"," no longer exists in pandas 2. Use ",[22,1922,1923],{},"with pd.ExcelWriter(...) as writer:"," so the file is written when the block ends, or ",[22,1926,617],{}," where the writer spans functions. Keep engine formatting code inside the block, replace ",[22,1929,1930],{},"writer.book = ...",[22,1932,1933],{},"writer.sheets = ..."," with ",[22,1936,648],{},[22,1938,840],{},", search the codebase for the old calls, and pin library versions so future removals arrive on your schedule.",[167,1941,1943],{"id":1942},"frequently-asked-questions","Frequently asked questions",[10,1945,1946,1949],{},[1872,1947,1948],{},"Why did ExcelWriter.save() stop working?","\nIt was deprecated in pandas 1.5 and removed in pandas 2.0. Saving now happens when the writer is closed — at the end of a with block or when you call writer.close().",[10,1951,1952,1955],{},[1872,1953,1954],{},"Is writer.close() the same as the old save()?","\nFor practical purposes, yes. close() writes the file and releases it. The difference is that you cannot keep writing after close(), whereas old code sometimes called save() and carried on.",[10,1957,1958,1961],{},[1872,1959,1960],{},"What replaces writer.book = load_workbook(...)?","\nOpen the writer in append mode — pd.ExcelWriter(path, mode=\"a\", engine=\"openpyxl\") — and choose what happens to existing sheets with if_sheet_exists. pandas loads the existing workbook for you.",[10,1963,1964,1967],{},[1872,1965,1966],{},"Can I still use xlsxwriter or openpyxl features on the sheets?","\nYes. writer.book and writer.sheets are still available to read inside the with block, so formatting code that calls the engine's API on them keeps working.",[167,1969,1971],{"id":1970},"related","Related",[1866,1973,1974,1980,1987,1994],{},[1869,1975,1976,1977,1979],{},"Up one level: ",[46,1978,49],{"href":48}," — every common error explained.",[1869,1981,1982,1986],{},[46,1983,1985],{"href":1984},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002F","Writing DataFrames to Excel with Pandas"," — the writer in depth.",[1869,1988,1989,1993],{},[46,1990,1992],{"href":1991},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fwrite-multiple-dataframes-to-one-excel-file\u002F","Write Multiple DataFrames to One Excel File"," — multi-sheet writers.",[1869,1995,1996,2000],{},[46,1997,1999],{"href":1998},"\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"," — choosing the engine.",[2002,2003,2004],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .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 .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":20,"searchDepth":266,"depth":266,"links":2006},[2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019],{"id":169,"depth":266,"text":170},{"id":211,"depth":266,"text":212},{"id":319,"depth":266,"text":320},{"id":423,"depth":266,"text":424},{"id":654,"depth":266,"text":655},{"id":815,"depth":266,"text":816},{"id":1009,"depth":266,"text":1010},{"id":1339,"depth":266,"text":1340},{"id":1847,"depth":266,"text":1848},{"id":1863,"depth":266,"text":1864},{"id":1913,"depth":266,"text":1914},{"id":1942,"depth":266,"text":1943},{"id":1970,"depth":266,"text":1971},"2026-09-23","pandas 2.0 removed ExcelWriter.save(), so old scripts fail with AttributeError. Replace it with a with block or close(), and fix the other removed writer APIs that usually break at the same time — book assignment, sheets assignment and handles.","md",[2024,2026,2028,2030],{"q":1948,"a":2025},"It was deprecated in pandas 1.5 and removed in pandas 2.0. Saving now happens when the writer is closed — at the end of a with block or when you call writer.close().",{"q":1954,"a":2027},"For practical purposes, yes. close() writes the file and releases it. The difference is that you cannot keep writing after close(), whereas old code sometimes called save() and carried on.",{"q":1960,"a":2029},"Open the writer in append mode — pd.ExcelWriter(path, mode=\"a\", engine=\"openpyxl\") — and choose what happens to existing sheets with if_sheet_exists. pandas loads the existing workbook for you.",{"q":1966,"a":2031},"Yes. writer.book and writer.sheets are still available to read inside the with block, so formatting code that calls the engine's API on them keeps working.","fix excelwriter object has no attribute save in pandas",null,{"breadcrumb":2035},[2036,2039,2042],{"name":2037,"item":2038},"Home","\u002F",{"name":2040,"item":2041},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":49,"item":48},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save",{"title":2045,"description":2046},"Fix AttributeError: 'OpenpyxlWriter' \u002F 'XlsxWriter' object has no attribute 'save'","Fix pandas ExcelWriter AttributeError \"no attribute 'save'\" after upgrading — use a with statement or writer.close(), replace writer.book and writer.sheets assignment with mode=\"a\", and keep formatting code that uses the engine objects.","fix-excelwriter-has-no-attribute-save","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save\u002Findex",[2050,2051,2052],"troubleshooting","writing-excel","pandas","how-to","VWJUaCLwDmfI7GDmuD0LyxgqCfSSM3VdshY-98TXvwM",[2056,2060],{"title":2057,"path":2058,"stem":2059,"children":-1},"Fix \"Excel Found Unreadable Content\" After Writing with Python","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python\u002Findex",{"title":2061,"path":2062,"stem":2063,"children":-1},"Fix Floating-Point Noise Like 0.1 + 0.2 ≠ 0.3 in Excel Output","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-floating-point-noise-in-excel-output","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-floating-point-noise-in-excel-output\u002Findex",[2065,2067],{"path":2066,"title":2040},"\u002Fgetting-started-with-python-excel-automation",{"path":2068,"title":49},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[2071,2075,2078,2081,2084,2087],{"path":2072,"title":2073,"kind":2074},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python","Handle Permission Denied When Writing Excel in Python","fix",{"path":2076,"title":2077,"kind":2053},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fwhat-openpyxl-loses-when-saving-an-existing-workbook","What openpyxl Loses When You Save an Existing Workbook",{"path":2079,"title":2080,"kind":2074},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","Fix Excel Serial Numbers Showing Instead of Dates",{"path":2082,"title":2083,"kind":2053},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-daylight-saving-time-in-excel-timestamps","Handle Daylight Saving Time in Excel Timestamps",{"path":2085,"title":2086,"kind":2053},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":2088,"title":2089,"kind":2090},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972065692]