[{"data":1,"prerenderedAt":2219},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor":2183,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor":2192,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor":2197,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor":2198},{"id":4,"title":5,"body":6,"dateModified":2149,"datePublished":2149,"description":2150,"extension":2151,"faq":2152,"keyword":2161,"kind":2162,"meta":2163,"navigation":367,"path":2172,"seo":2173,"slug":2176,"stem":2177,"topics":2178,"type":2181,"__hash__":2182},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor\u002Findex.md","Fix xlsxwriter \"Exception caught in workbook destructor\"",{"type":7,"value":8,"toc":2136},"minimark",[9,13,24,41,46,72,82,86,95,194,197,212,223,326,330,336,452,455,459,471,562,579,694,698,708,836,839,934,1094,1098,1106,1206,1213,1371,1375,1378,1824,1827,1981,1985,2048,2052,2064,2068,2074,2080,2086,2092,2096,2132],[10,11,12],"p",{},"This message appears in older scripts and older environments:",[14,15,21],"pre",{"className":16,"code":18,"language":19,"meta":20},[17],"language-text","Exception ignored in: \u003Cfunction Workbook.__del__ at 0x7f...>\nException: Exception caught in workbook destructor. Explicit close() may be required for workbook.\n","text","",[22,23,18],"code",{"__ignoreMap":20},[10,25,26,27,30,31,34,35,40],{},"It means xlsxwriter tried to save the workbook when Python destroyed the ",[22,28,29],{},"Workbook"," object, because the code never called ",[22,32,33],{},"close()",", and saving failed. In current xlsxwriter the message no longer exists, but the underlying mistake does, and it has become quieter: a workbook that is never closed is simply never written. This page explains both, and how to close workbooks so the file is always written or the failure is always reported. It is part of ",[36,37,39],"a",{"href":38},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",".",[42,43,45],"h2",{"id":44},"prerequisites","Prerequisites",[14,47,51],{"className":48,"code":49,"language":50,"meta":20,"style":20},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install xlsxwriter pandas\n","bash",[22,52,53],{"__ignoreMap":20},[54,55,58,62,66,69],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"sMTad","pip",[54,63,65],{"class":64},"srMev"," install",[54,67,68],{"class":64}," xlsxwriter",[54,70,71],{"class":64}," pandas\n",[10,73,74,75,78,79,81],{},"The examples use xlsxwriter 3.2. xlsxwriter builds the whole file in memory (or temporary files) and writes the ",[22,76,77],{},".xlsx"," only in ",[22,80,33],{},"; nothing reaches the target path before that.",[42,83,85],{"id":84},"where-the-message-comes-from","Where the message comes from",[10,87,88,89,91,92,94],{},"In xlsxwriter 1.0 and earlier, ",[22,90,29],{}," had a destructor that called ",[22,93,33],{}," if the code had not, and turned any error it hit into this exception:",[14,96,100],{"className":97,"code":98,"language":99,"meta":20,"style":20},"language-python shiki shiki-themes github-light github-dark-high-contrast","# xlsxwriter 1.0.x, workbook.py\ndef __del__(self):\n    \"\"\"Close file in destructor if it hasn't been closed explicitly.\"\"\"\n    try:\n        if not self.fileclosed:\n            self.close()\n    except:\n        raise Exception(\"Exception caught in workbook destructor. \"\n                        \"Explicit close() may be required for workbook.\")\n","python",[22,101,102,108,123,129,138,153,162,170,185],{"__ignoreMap":20},[54,103,104],{"class":56,"line":57},[54,105,107],{"class":106},"s-wDw","# xlsxwriter 1.0.x, workbook.py\n",[54,109,111,115,119],{"class":56,"line":110},2,[54,112,114],{"class":113},"s-kum","def",[54,116,118],{"class":117},"sP0c6"," __del__",[54,120,122],{"class":121},"skGVy","(self):\n",[54,124,126],{"class":56,"line":125},3,[54,127,128],{"class":64},"    \"\"\"Close file in destructor if it hasn't been closed explicitly.\"\"\"\n",[54,130,132,135],{"class":56,"line":131},4,[54,133,134],{"class":113},"    try",[54,136,137],{"class":121},":\n",[54,139,141,144,147,150],{"class":56,"line":140},5,[54,142,143],{"class":113},"        if",[54,145,146],{"class":113}," not",[54,148,149],{"class":117}," self",[54,151,152],{"class":121},".fileclosed:\n",[54,154,156,159],{"class":56,"line":155},6,[54,157,158],{"class":117},"            self",[54,160,161],{"class":121},".close()\n",[54,163,165,168],{"class":56,"line":164},7,[54,166,167],{"class":113},"    except",[54,169,137],{"class":121},[54,171,173,176,179,182],{"class":56,"line":172},8,[54,174,175],{"class":113},"        raise",[54,177,178],{"class":117}," Exception",[54,180,181],{"class":121},"(",[54,183,184],{"class":64},"\"Exception caught in workbook destructor. \"\n",[54,186,188,191],{"class":56,"line":187},9,[54,189,190],{"class":64},"                        \"Explicit close() may be required for workbook.\"",[54,192,193],{"class":121},")\n",[10,195,196],{},"Exceptions raised in a destructor cannot propagate, so Python printed it as \"Exception ignored\" and carried on, and the real error — a missing directory, a file locked by Excel — was swallowed. Version 1.1 removed the destructor. If you see this message, the environment has a very old xlsxwriter; upgrading is worthwhile, but the fix in the code is the same either way. Check which version the failing job actually runs, which is not always the one on your laptop:",[14,198,200],{"className":48,"code":199,"language":50,"meta":20,"style":20},"python -c \"import xlsxwriter; print(xlsxwriter.__version__)\"\n",[22,201,202],{"__ignoreMap":20},[54,203,204,206,209],{"class":56,"line":57},[54,205,99],{"class":60},[54,207,208],{"class":117}," -c",[54,210,211],{"class":64}," \"import xlsxwriter; print(xlsxwriter.__version__)\"\n",[10,213,214,215,219,220,222],{},"Old versions usually survive in long-lived servers, frozen virtual environments and executables built years ago. Pin a current version in the project's requirements, as described in ",[36,216,218],{"href":217},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fset-up-an-excel-automation-project-with-uv\u002F","Set Up an Excel Automation Project with uv",", so every environment that runs the report gets the same behaviour. After upgrading, a missing ",[22,221,33],{}," no longer produces the message at all, which makes the next two steps essential rather than optional.",[224,225,233,234,233,238,233,242,233,250,233,260,233,271,233,277,233,283,233,288,233,292,233,296,233,301,233,308,233,313,233,317,233,320,233,323],"svg",{"viewBox":226,"role":227,"ariaLabelledBy":228,"xmlns":231,"style":232},"4 0 752 207","img",[229,230],"xd-versions-t","xd-versions-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[235,236,237],"title",{"id":229},"An unclosed workbook, then and now",[239,240,241],"desc",{"id":230},"Two panels. In xlsxwriter 1.0 and earlier, the destructor saved an unclosed workbook when it was garbage collected and printed this exception if saving failed. From version 1.1 there is no destructor, so an unclosed workbook is never written and nothing is printed.",[243,244],"rect",{"x":245,"y":246,"width":247,"height":248,"fill":249},"4","0","752","207","#ffffff",[243,251],{"x":252,"y":253,"width":254,"height":255,"rx":256,"fill":257,"stroke":258,"style":259},"20.0","16.0","351.0","175.0","14","#fdefd8","var(--gold,#b4740a)","stroke-width:2px",[261,262,264],"g",{"transform":263},"translate(182.5,29.0) scale(1.083)",[265,266],"path",{"d":267,"fill":268,"stroke":269,"style":270},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","none","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[19,272,276],{"x":273,"y":274,"style":275},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xlsxwriter 1.0 and earlier",[56,278],{"x1":279,"y1":280,"x2":281,"y2":280,"stroke":258,"style":282},"36.0","91.0","355.0","stroke-width:1px",[19,284,287],{"x":273,"y":285,"style":286},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","destructor calls close()",[19,289,291],{"x":273,"y":290,"style":286},"137.0","errors become this message",[19,293,295],{"x":273,"y":294,"style":286},"163.0","real cause is hidden",[243,297],{"x":298,"y":253,"width":254,"height":255,"rx":256,"fill":299,"stroke":300,"style":259},"389.0","#fce9e9","var(--danger,#dc2626)",[261,302,304],{"transform":303},"translate(551.5,29.0) scale(1.083)",[265,305],{"d":306,"fill":268,"stroke":307,"style":270},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)",[19,309,312],{"x":310,"y":274,"style":311},"564.5","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","xlsxwriter 1.1 and later",[56,314],{"x1":315,"y1":280,"x2":316,"y2":280,"stroke":300,"style":282},"405.0","724.0",[19,318,319],{"x":310,"y":285,"style":286},"no destructor",[19,321,322],{"x":310,"y":290,"style":286},"no file written",[19,324,325],{"x":310,"y":294,"style":286},"no message at all",[42,327,329],{"id":328},"step-1-see-the-silent-version","Step 1 — See the silent version",[10,331,332,333,335],{},"In a current environment, forgetting ",[22,334,33],{}," produces no error and no file:",[14,337,339],{"className":97,"code":338,"language":99,"meta":20,"style":20},"import gc\nimport os\nimport xlsxwriter\n\ndef build_report():\n    wb = xlsxwriter.Workbook(\"never_closed.xlsx\")\n    wb.add_worksheet().write(\"A1\", \"Sales\")\n    # no wb.close()\n\nbuild_report()\ngc.collect()\nprint(\"file written:\", os.path.exists(\"never_closed.xlsx\"))\n",[22,340,341,349,356,363,369,380,396,412,417,421,427,433],{"__ignoreMap":20},[54,342,343,346],{"class":56,"line":57},[54,344,345],{"class":113},"import",[54,347,348],{"class":121}," gc\n",[54,350,351,353],{"class":56,"line":110},[54,352,345],{"class":113},[54,354,355],{"class":121}," os\n",[54,357,358,360],{"class":56,"line":125},[54,359,345],{"class":113},[54,361,362],{"class":121}," xlsxwriter\n",[54,364,365],{"class":56,"line":131},[54,366,368],{"emptyLinePlaceholder":367},true,"\n",[54,370,371,373,377],{"class":56,"line":140},[54,372,114],{"class":113},[54,374,376],{"class":375},"s_Opv"," build_report",[54,378,379],{"class":121},"():\n",[54,381,382,385,388,391,394],{"class":56,"line":155},[54,383,384],{"class":121},"    wb ",[54,386,387],{"class":113},"=",[54,389,390],{"class":121}," xlsxwriter.Workbook(",[54,392,393],{"class":64},"\"never_closed.xlsx\"",[54,395,193],{"class":121},[54,397,398,401,404,407,410],{"class":56,"line":164},[54,399,400],{"class":121},"    wb.add_worksheet().write(",[54,402,403],{"class":64},"\"A1\"",[54,405,406],{"class":121},", ",[54,408,409],{"class":64},"\"Sales\"",[54,411,193],{"class":121},[54,413,414],{"class":56,"line":172},[54,415,416],{"class":106},"    # no wb.close()\n",[54,418,419],{"class":56,"line":187},[54,420,368],{"emptyLinePlaceholder":367},[54,422,424],{"class":56,"line":423},10,[54,425,426],{"class":121},"build_report()\n",[54,428,430],{"class":56,"line":429},11,[54,431,432],{"class":121},"gc.collect()\n",[54,434,436,439,441,444,447,449],{"class":56,"line":435},12,[54,437,438],{"class":117},"print",[54,440,181],{"class":121},[54,442,443],{"class":64},"\"file written:\"",[54,445,446],{"class":121},", os.path.exists(",[54,448,393],{"class":64},[54,450,451],{"class":121},"))\n",[10,453,454],{},"The function returns normally, the process exits with status 0, and any monitoring that only watches exit codes sees a healthy job. A scheduled job with this bug \"succeeds\" every night and delivers nothing, or, worse, attaches last night's file that happens to be lying in the folder.",[42,456,458],{"id":457},"step-2-close-with-a-context-manager","Step 2 — Close with a context manager",[10,460,461,463,464,467,468,470],{},[22,462,29],{}," is a context manager: ",[22,465,466],{},"with"," calls ",[22,469,33],{}," when the block ends, including when it ends because of an error. That guarantees the file is written or the failure is raised at a known place:",[14,472,474],{"className":97,"code":473,"language":99,"meta":20,"style":20},"with xlsxwriter.Workbook(\"report.xlsx\") as wb:\n    ws = wb.add_worksheet(\"Sales\")\n    ws.write_row(\"A1\", [\"Region\", \"Total\"])\n    ws.write_row(\"A2\", [\"North\", 12500])\nprint(\"file written:\", os.path.exists(\"report.xlsx\"))\n",[22,475,476,494,508,529,548],{"__ignoreMap":20},[54,477,478,480,482,485,488,491],{"class":56,"line":57},[54,479,466],{"class":113},[54,481,390],{"class":121},[54,483,484],{"class":64},"\"report.xlsx\"",[54,486,487],{"class":121},") ",[54,489,490],{"class":113},"as",[54,492,493],{"class":121}," wb:\n",[54,495,496,499,501,504,506],{"class":56,"line":110},[54,497,498],{"class":121},"    ws ",[54,500,387],{"class":113},[54,502,503],{"class":121}," wb.add_worksheet(",[54,505,409],{"class":64},[54,507,193],{"class":121},[54,509,510,513,515,518,521,523,526],{"class":56,"line":125},[54,511,512],{"class":121},"    ws.write_row(",[54,514,403],{"class":64},[54,516,517],{"class":121},", [",[54,519,520],{"class":64},"\"Region\"",[54,522,406],{"class":121},[54,524,525],{"class":64},"\"Total\"",[54,527,528],{"class":121},"])\n",[54,530,531,533,536,538,541,543,546],{"class":56,"line":131},[54,532,512],{"class":121},[54,534,535],{"class":64},"\"A2\"",[54,537,517],{"class":121},[54,539,540],{"class":64},"\"North\"",[54,542,406],{"class":121},[54,544,545],{"class":117},"12500",[54,547,528],{"class":121},[54,549,550,552,554,556,558,560],{"class":56,"line":140},[54,551,438],{"class":117},[54,553,181],{"class":121},[54,555,443],{"class":64},[54,557,446],{"class":121},[54,559,484],{"class":64},[54,561,451],{"class":121},[10,563,564,565,568,569,572,573,575,576,578],{},"For pandas, the same applies to ",[22,566,567],{},"ExcelWriter",". Its old ",[22,570,571],{},"save()"," method was removed in pandas 2.0; use ",[22,574,466],{}," or ",[22,577,33],{},":",[14,580,582],{"className":97,"code":581,"language":99,"meta":20,"style":20},"import pandas as pd\n\nwith pd.ExcelWriter(\"pandas_report.xlsx\", engine=\"xlsxwriter\") as writer:\n    pd.DataFrame({\"Region\": [\"North\"], \"Total\": [12500]}).to_excel(writer, index=False)\nprint(\"has save():\", hasattr(writer, \"save\"), \"| file written:\", os.path.exists(\"pandas_report.xlsx\"))\n",[22,583,584,596,600,628,662],{"__ignoreMap":20},[54,585,586,588,591,593],{"class":56,"line":57},[54,587,345],{"class":113},[54,589,590],{"class":121}," pandas ",[54,592,490],{"class":113},[54,594,595],{"class":121}," pd\n",[54,597,598],{"class":56,"line":110},[54,599,368],{"emptyLinePlaceholder":367},[54,601,602,604,607,610,612,616,618,621,623,625],{"class":56,"line":125},[54,603,466],{"class":113},[54,605,606],{"class":121}," pd.ExcelWriter(",[54,608,609],{"class":64},"\"pandas_report.xlsx\"",[54,611,406],{"class":121},[54,613,615],{"class":614},"sa561","engine",[54,617,387],{"class":113},[54,619,620],{"class":64},"\"xlsxwriter\"",[54,622,487],{"class":121},[54,624,490],{"class":113},[54,626,627],{"class":121}," writer:\n",[54,629,630,633,635,638,640,643,645,647,649,652,655,657,660],{"class":56,"line":131},[54,631,632],{"class":121},"    pd.DataFrame({",[54,634,520],{"class":64},[54,636,637],{"class":121},": [",[54,639,540],{"class":64},[54,641,642],{"class":121},"], ",[54,644,525],{"class":64},[54,646,637],{"class":121},[54,648,545],{"class":117},[54,650,651],{"class":121},"]}).to_excel(writer, ",[54,653,654],{"class":614},"index",[54,656,387],{"class":113},[54,658,659],{"class":117},"False",[54,661,193],{"class":121},[54,663,664,666,668,671,673,676,679,682,685,688,690,692],{"class":56,"line":140},[54,665,438],{"class":117},[54,667,181],{"class":121},[54,669,670],{"class":64},"\"has save():\"",[54,672,406],{"class":121},[54,674,675],{"class":117},"hasattr",[54,677,678],{"class":121},"(writer, ",[54,680,681],{"class":64},"\"save\"",[54,683,684],{"class":121},"), ",[54,686,687],{"class":64},"\"| file written:\"",[54,689,446],{"class":121},[54,691,609],{"class":64},[54,693,451],{"class":121},[42,695,697],{"id":696},"step-3-handle-the-errors-close-raises","Step 3 — Handle the errors close() raises",[10,699,700,701,703,704,707],{},"Because the file is only created in ",[22,702,33],{},", that is where file-system problems surface, as ",[22,705,706],{},"xlsxwriter.exceptions.FileCreateError",". The usual causes are a directory that does not exist, a folder without write permission, and on Windows, the same file open in Excel:",[14,709,711],{"className":97,"code":710,"language":99,"meta":20,"style":20},"from xlsxwriter.exceptions import FileCreateError\n\nfor path in [\"missing_dir\u002Freport.xlsx\", \"locked\u002Freport.xlsx\"]:\n    wb = xlsxwriter.Workbook(path)\n    wb.add_worksheet().write(\"A1\", \"x\")\n    try:\n        wb.close()\n    except FileCreateError as e:\n        print(f\"{path}: {e}\")\n",[22,712,713,726,730,755,764,777,783,788,800],{"__ignoreMap":20},[54,714,715,718,721,723],{"class":56,"line":57},[54,716,717],{"class":113},"from",[54,719,720],{"class":121}," xlsxwriter.exceptions ",[54,722,345],{"class":113},[54,724,725],{"class":121}," FileCreateError\n",[54,727,728],{"class":56,"line":110},[54,729,368],{"emptyLinePlaceholder":367},[54,731,732,735,738,741,744,747,749,752],{"class":56,"line":125},[54,733,734],{"class":113},"for",[54,736,737],{"class":121}," path ",[54,739,740],{"class":113},"in",[54,742,743],{"class":121}," [",[54,745,746],{"class":64},"\"missing_dir\u002Freport.xlsx\"",[54,748,406],{"class":121},[54,750,751],{"class":64},"\"locked\u002Freport.xlsx\"",[54,753,754],{"class":121},"]:\n",[54,756,757,759,761],{"class":56,"line":131},[54,758,384],{"class":121},[54,760,387],{"class":113},[54,762,763],{"class":121}," xlsxwriter.Workbook(path)\n",[54,765,766,768,770,772,775],{"class":56,"line":140},[54,767,400],{"class":121},[54,769,403],{"class":64},[54,771,406],{"class":121},[54,773,774],{"class":64},"\"x\"",[54,776,193],{"class":121},[54,778,779,781],{"class":56,"line":155},[54,780,134],{"class":113},[54,782,137],{"class":121},[54,784,785],{"class":56,"line":164},[54,786,787],{"class":121},"        wb.close()\n",[54,789,790,792,795,797],{"class":56,"line":172},[54,791,167],{"class":113},[54,793,794],{"class":121}," FileCreateError ",[54,796,490],{"class":113},[54,798,799],{"class":121}," e:\n",[54,801,802,805,807,810,813,817,819,822,825,827,830,832,834],{"class":56,"line":187},[54,803,804],{"class":117},"        print",[54,806,181],{"class":121},[54,808,809],{"class":113},"f",[54,811,812],{"class":64},"\"",[54,814,816],{"class":815},"sSjpA","{",[54,818,265],{"class":121},[54,820,821],{"class":815},"}",[54,823,824],{"class":64},": ",[54,826,816],{"class":815},[54,828,829],{"class":121},"e",[54,831,821],{"class":815},[54,833,812],{"class":64},[54,835,193],{"class":121},[10,837,838],{},"For an interactive script, a retry prompt is kind to the user who left the file open. For a scheduled job, create the directory first and let the error fail the job loudly:",[14,840,842],{"className":97,"code":841,"language":99,"meta":20,"style":20},"from pathlib import Path\n\nout = Path(\"output\u002F2026-09\u002Freport.xlsx\")\nout.parent.mkdir(parents=True, exist_ok=True)\nwith xlsxwriter.Workbook(out) as wb:\n    wb.add_worksheet().write(\"A1\", \"x\")\nprint(\"written to\", out)\n",[22,843,844,856,860,875,899,910,922],{"__ignoreMap":20},[54,845,846,848,851,853],{"class":56,"line":57},[54,847,717],{"class":113},[54,849,850],{"class":121}," pathlib ",[54,852,345],{"class":113},[54,854,855],{"class":121}," Path\n",[54,857,858],{"class":56,"line":110},[54,859,368],{"emptyLinePlaceholder":367},[54,861,862,865,867,870,873],{"class":56,"line":125},[54,863,864],{"class":121},"out ",[54,866,387],{"class":113},[54,868,869],{"class":121}," Path(",[54,871,872],{"class":64},"\"output\u002F2026-09\u002Freport.xlsx\"",[54,874,193],{"class":121},[54,876,877,880,883,885,888,890,893,895,897],{"class":56,"line":131},[54,878,879],{"class":121},"out.parent.mkdir(",[54,881,882],{"class":614},"parents",[54,884,387],{"class":113},[54,886,887],{"class":117},"True",[54,889,406],{"class":121},[54,891,892],{"class":614},"exist_ok",[54,894,387],{"class":113},[54,896,887],{"class":117},[54,898,193],{"class":121},[54,900,901,903,906,908],{"class":56,"line":140},[54,902,466],{"class":113},[54,904,905],{"class":121}," xlsxwriter.Workbook(out) ",[54,907,490],{"class":113},[54,909,493],{"class":121},[54,911,912,914,916,918,920],{"class":56,"line":155},[54,913,400],{"class":121},[54,915,403],{"class":64},[54,917,406],{"class":121},[54,919,774],{"class":64},[54,921,193],{"class":121},[54,923,924,926,928,931],{"class":56,"line":164},[54,925,438],{"class":117},[54,927,181],{"class":121},[54,929,930],{"class":64},"\"written to\"",[54,932,933],{"class":121},", out)\n",[224,935,233,940,233,943,233,946,233,949,233,956,233,964,233,970,233,974,233,979,233,983,233,989,233,994,233,998,233,1005,233,1009,233,1012,233,1016,233,1019,233,1022,233,1027,233,1033,233,1037,233,1041,233,1045,233,1048,233,1051,233,1057,233,1061,233,1064,233,1068,233,1071,233,1076,233,1083,233,1088,233,1091],{"viewBox":936,"role":227,"ariaLabelledBy":937,"xmlns":231,"style":232},"4 0 752 160",[938,939],"xd-close-t","xd-close-d",[235,941,942],{"id":938},"What happens in close()",[239,944,945],{"id":939},"The workbook is assembled in memory. close() creates the target file, which is where missing directories, permissions and files locked by Excel raise FileCreateError, then writes the parts and zips them. Only after close() returns is the file complete.",[243,947],{"x":245,"y":246,"width":247,"height":948,"fill":249},"160",[243,950],{"x":252,"y":253,"width":951,"height":952,"rx":953,"fill":954,"stroke":955,"style":259},"115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)",[261,957,959],{"transform":958},"translate(65.6,29.0) scale(1.000)",[265,960],{"d":961,"fill":268,"stroke":962,"style":963},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[19,965,969],{"x":966,"y":967,"style":968},"77.6","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Build in",[19,971,973],{"x":966,"y":972,"style":968},"93.0","memory",[19,975,978],{"x":966,"y":976,"style":977},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","add_worksheet",[19,980,982],{"x":966,"y":981,"style":977},"130.0","\u002F write",[56,984],{"x1":985,"y1":986,"x2":987,"y2":986,"stroke":988,"style":259},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[990,991],"polygon",{"points":992,"fill":993},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[243,995],{"x":996,"y":253,"width":951,"height":952,"rx":953,"fill":997,"stroke":988,"style":259},"171.2","#f0f4ff",[261,999,1001],{"transform":1000},"translate(216.8,46.5) scale(1.000)",[265,1002],{"d":1003,"fill":268,"stroke":1004,"style":963},"M5 12.5l4.5 4.5L19 7","var(--brand-strong,#4338ca)",[19,1006,33],{"x":1007,"y":972,"style":1008},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[19,1010,1011],{"x":1007,"y":976,"style":977},"or end of with",[56,1013],{"x1":1014,"y1":986,"x2":1015,"y2":986,"stroke":988,"style":259},"291.4","313.4",[990,1017],{"points":1018,"fill":993},"317.4,80.0 308.4,85.0 308.4,75.0",[243,1020],{"x":1021,"y":253,"width":951,"height":952,"rx":953,"fill":257,"stroke":258,"style":259},"322.4",[261,1023,1025],{"transform":1024},"translate(368.0,37.8) scale(1.000)",[265,1026],{"d":306,"fill":268,"stroke":269,"style":963},[19,1028,1032],{"x":1029,"y":1030,"style":1031},"380.0","84.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Create file",[19,1034,1036],{"x":1029,"y":1035,"style":977},"103.8","FileCreateError",[19,1038,1040],{"x":1029,"y":1039,"style":977},"121.2","here",[56,1042],{"x1":1043,"y1":986,"x2":1044,"y2":986,"stroke":988,"style":259},"442.6","464.6",[990,1046],{"points":1047,"fill":993},"468.6,80.0 459.6,85.0 459.6,75.0",[243,1049],{"x":1050,"y":253,"width":951,"height":952,"rx":953,"fill":997,"stroke":988,"style":259},"473.6",[261,1052,1054],{"transform":1053},"translate(519.2,46.5) scale(1.000)",[265,1055],{"d":1056,"fill":268,"stroke":1004,"style":963},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6",[19,1058,1060],{"x":1059,"y":972,"style":1008},"531.2","Write and zip",[19,1062,1063],{"x":1059,"y":976,"style":977},"XML parts",[56,1065],{"x1":1066,"y1":986,"x2":1067,"y2":986,"stroke":988,"style":259},"593.8","615.8",[990,1069],{"points":1070,"fill":993},"619.8,80.0 610.8,85.0 610.8,75.0",[243,1072],{"x":1073,"y":253,"width":951,"height":952,"rx":953,"fill":1074,"stroke":1075,"style":259},"624.8","#d9f4f1","var(--teal,#0f9488)",[261,1077,1079],{"transform":1078},"translate(670.4,37.8) scale(1.000)",[265,1080],{"d":1081,"fill":268,"stroke":1082,"style":963},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--teal-ink,#0b6157)",[19,1084,1087],{"x":1085,"y":1030,"style":1086},"682.4","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Complete",[19,1089,77],{"x":1085,"y":1090,"style":1086},"101.8",[19,1092,1093],{"x":1085,"y":1039,"style":977},"safe to send",[42,1095,1097],{"id":1096},"step-4-dont-ship-a-half-built-report","Step 4 — Don't ship a half-built report",[10,1099,1100,1101,1105],{},"The context manager closes the workbook even when the code inside the block fails, so an error halfway through leaves a ",[1102,1103,1104],"em",{},"valid but incomplete"," file at the target path:",[14,1107,1109],{"className":97,"code":1108,"language":99,"meta":20,"style":20},"try:\n    with xlsxwriter.Workbook(\"half_built.xlsx\") as wb:\n        ws = wb.add_worksheet()\n        ws.write(\"A1\", \"Summary\")\n        raise RuntimeError(\"database query failed\")\nexcept RuntimeError as e:\n    print(\"error:\", e, \"| file exists anyway:\", os.path.exists(\"half_built.xlsx\"))\n",[22,1110,1111,1118,1134,1144,1158,1172,1184],{"__ignoreMap":20},[54,1112,1113,1116],{"class":56,"line":57},[54,1114,1115],{"class":113},"try",[54,1117,137],{"class":121},[54,1119,1120,1123,1125,1128,1130,1132],{"class":56,"line":110},[54,1121,1122],{"class":113},"    with",[54,1124,390],{"class":121},[54,1126,1127],{"class":64},"\"half_built.xlsx\"",[54,1129,487],{"class":121},[54,1131,490],{"class":113},[54,1133,493],{"class":121},[54,1135,1136,1139,1141],{"class":56,"line":125},[54,1137,1138],{"class":121},"        ws ",[54,1140,387],{"class":113},[54,1142,1143],{"class":121}," wb.add_worksheet()\n",[54,1145,1146,1149,1151,1153,1156],{"class":56,"line":131},[54,1147,1148],{"class":121},"        ws.write(",[54,1150,403],{"class":64},[54,1152,406],{"class":121},[54,1154,1155],{"class":64},"\"Summary\"",[54,1157,193],{"class":121},[54,1159,1160,1162,1165,1167,1170],{"class":56,"line":140},[54,1161,175],{"class":113},[54,1163,1164],{"class":117}," RuntimeError",[54,1166,181],{"class":121},[54,1168,1169],{"class":64},"\"database query failed\"",[54,1171,193],{"class":121},[54,1173,1174,1177,1179,1182],{"class":56,"line":155},[54,1175,1176],{"class":113},"except",[54,1178,1164],{"class":117},[54,1180,1181],{"class":113}," as",[54,1183,799],{"class":121},[54,1185,1186,1189,1191,1194,1197,1200,1202,1204],{"class":56,"line":164},[54,1187,1188],{"class":117},"    print",[54,1190,181],{"class":121},[54,1192,1193],{"class":64},"\"error:\"",[54,1195,1196],{"class":121},", e, ",[54,1198,1199],{"class":64},"\"| file exists anyway:\"",[54,1201,446],{"class":121},[54,1203,1127],{"class":64},[54,1205,451],{"class":121},[10,1207,1208,1209,1212],{},"If a later step emails or uploads whatever is in the output folder, the recipients get a report with a title and no numbers. Build to a temporary name and rename only on success; ",[22,1210,1211],{},"os.replace"," is atomic on the same file system, so readers see either the old file or the complete new one:",[14,1214,1216],{"className":97,"code":1215,"language":99,"meta":20,"style":20},"def write_report(path, build):\n    tmp = Path(path).with_suffix(\".tmp.xlsx\")\n    try:\n        with xlsxwriter.Workbook(tmp) as wb:\n            build(wb)\n    except Exception:\n        tmp.unlink(missing_ok=True)\n        raise\n    os.replace(tmp, path)\n\nwrite_report(\"final.xlsx\", lambda wb: wb.add_worksheet().write(\"A1\", \"complete\"))\nprint(sorted(p for p in os.listdir(\".\") if p.startswith((\"final\", \"half\"))))\n",[22,1217,1218,1228,1243,1249,1261,1266,1274,1288,1293,1298,1302,1327],{"__ignoreMap":20},[54,1219,1220,1222,1225],{"class":56,"line":57},[54,1221,114],{"class":113},[54,1223,1224],{"class":375}," write_report",[54,1226,1227],{"class":121},"(path, build):\n",[54,1229,1230,1233,1235,1238,1241],{"class":56,"line":110},[54,1231,1232],{"class":121},"    tmp ",[54,1234,387],{"class":113},[54,1236,1237],{"class":121}," Path(path).with_suffix(",[54,1239,1240],{"class":64},"\".tmp.xlsx\"",[54,1242,193],{"class":121},[54,1244,1245,1247],{"class":56,"line":125},[54,1246,134],{"class":113},[54,1248,137],{"class":121},[54,1250,1251,1254,1257,1259],{"class":56,"line":131},[54,1252,1253],{"class":113},"        with",[54,1255,1256],{"class":121}," xlsxwriter.Workbook(tmp) ",[54,1258,490],{"class":113},[54,1260,493],{"class":121},[54,1262,1263],{"class":56,"line":140},[54,1264,1265],{"class":121},"            build(wb)\n",[54,1267,1268,1270,1272],{"class":56,"line":155},[54,1269,167],{"class":113},[54,1271,178],{"class":117},[54,1273,137],{"class":121},[54,1275,1276,1279,1282,1284,1286],{"class":56,"line":164},[54,1277,1278],{"class":121},"        tmp.unlink(",[54,1280,1281],{"class":614},"missing_ok",[54,1283,387],{"class":113},[54,1285,887],{"class":117},[54,1287,193],{"class":121},[54,1289,1290],{"class":56,"line":172},[54,1291,1292],{"class":113},"        raise\n",[54,1294,1295],{"class":56,"line":187},[54,1296,1297],{"class":121},"    os.replace(tmp, path)\n",[54,1299,1300],{"class":56,"line":423},[54,1301,368],{"emptyLinePlaceholder":367},[54,1303,1304,1307,1310,1312,1315,1318,1320,1322,1325],{"class":56,"line":429},[54,1305,1306],{"class":121},"write_report(",[54,1308,1309],{"class":64},"\"final.xlsx\"",[54,1311,406],{"class":121},[54,1313,1314],{"class":113},"lambda",[54,1316,1317],{"class":121}," wb: wb.add_worksheet().write(",[54,1319,403],{"class":64},[54,1321,406],{"class":121},[54,1323,1324],{"class":64},"\"complete\"",[54,1326,451],{"class":121},[54,1328,1329,1331,1333,1336,1339,1341,1344,1346,1349,1352,1354,1357,1360,1363,1365,1368],{"class":56,"line":435},[54,1330,438],{"class":117},[54,1332,181],{"class":121},[54,1334,1335],{"class":117},"sorted",[54,1337,1338],{"class":121},"(p ",[54,1340,734],{"class":113},[54,1342,1343],{"class":121}," p ",[54,1345,740],{"class":113},[54,1347,1348],{"class":121}," os.listdir(",[54,1350,1351],{"class":64},"\".\"",[54,1353,487],{"class":121},[54,1355,1356],{"class":113},"if",[54,1358,1359],{"class":121}," p.startswith((",[54,1361,1362],{"class":64},"\"final\"",[54,1364,406],{"class":121},[54,1366,1367],{"class":64},"\"half\"",[54,1369,1370],{"class":121},"))))\n",[42,1372,1374],{"id":1373},"step-5-check-the-file-before-delivering-it","Step 5 — Check the file before delivering it",[10,1376,1377],{},"Closing correctly guarantees that a written file is complete; it does not guarantee that the file contains what the recipient expects. A last check before the email or upload step catches the rest: the file exists, was written by this run, opens, and has the sheets and rows it should. It costs a fraction of a second:",[14,1379,1381],{"className":97,"code":1380,"language":99,"meta":20,"style":20},"import time\nfrom openpyxl import load_workbook\n\ndef check_report(path, started, sheets, min_rows=2):\n    p = Path(path)\n    assert p.exists(), f\"{p} was not written\"\n    assert p.stat().st_mtime >= started, f\"{p} is left over from an earlier run\"\n    wb = load_workbook(p, read_only=True)\n    try:\n        missing = set(sheets) - set(wb.sheetnames)\n        assert not missing, f\"{p} is missing sheets {missing}\"\n        for name in sheets:\n            rows = sum(1 for _ in wb[name].iter_rows(values_only=True))\n            assert rows >= min_rows, f\"{p}:{name} has only {rows} rows\"\n    finally:\n        wb.close()\n    return \"ok\"\n\nstarted = time.time() - 1\nwith xlsxwriter.Workbook(\"checked.xlsx\") as wb:\n    ws = wb.add_worksheet(\"Sales\")\n    ws.write_row(\"A1\", [\"Region\", \"Total\"])\n    ws.write_row(\"A2\", [\"North\", 12500])\nprint(check_report(\"checked.xlsx\", started, [\"Sales\"]))\ntry:\n    check_report(\"half_built.xlsx\", started, [\"Sales\"])\nexcept AssertionError as e:\n    print(\"blocked:\", e)\n",[22,1382,1383,1390,1402,1406,1424,1434,1455,1481,1499,1505,1526,1559,1572,1608,1654,1662,1667,1676,1681,1697,1713,1726,1743,1760,1778,1785,1799,1811],{"__ignoreMap":20},[54,1384,1385,1387],{"class":56,"line":57},[54,1386,345],{"class":113},[54,1388,1389],{"class":121}," time\n",[54,1391,1392,1394,1397,1399],{"class":56,"line":110},[54,1393,717],{"class":113},[54,1395,1396],{"class":121}," openpyxl ",[54,1398,345],{"class":113},[54,1400,1401],{"class":121}," load_workbook\n",[54,1403,1404],{"class":56,"line":125},[54,1405,368],{"emptyLinePlaceholder":367},[54,1407,1408,1410,1413,1416,1418,1421],{"class":56,"line":131},[54,1409,114],{"class":113},[54,1411,1412],{"class":375}," check_report",[54,1414,1415],{"class":121},"(path, started, sheets, min_rows",[54,1417,387],{"class":113},[54,1419,1420],{"class":117},"2",[54,1422,1423],{"class":121},"):\n",[54,1425,1426,1429,1431],{"class":56,"line":140},[54,1427,1428],{"class":121},"    p ",[54,1430,387],{"class":113},[54,1432,1433],{"class":121}," Path(path)\n",[54,1435,1436,1439,1442,1444,1446,1448,1450,1452],{"class":56,"line":155},[54,1437,1438],{"class":113},"    assert",[54,1440,1441],{"class":121}," p.exists(), ",[54,1443,809],{"class":113},[54,1445,812],{"class":64},[54,1447,816],{"class":815},[54,1449,10],{"class":121},[54,1451,821],{"class":815},[54,1453,1454],{"class":64}," was not written\"\n",[54,1456,1457,1459,1462,1465,1468,1470,1472,1474,1476,1478],{"class":56,"line":164},[54,1458,1438],{"class":113},[54,1460,1461],{"class":121}," p.stat().st_mtime ",[54,1463,1464],{"class":113},">=",[54,1466,1467],{"class":121}," started, ",[54,1469,809],{"class":113},[54,1471,812],{"class":64},[54,1473,816],{"class":815},[54,1475,10],{"class":121},[54,1477,821],{"class":815},[54,1479,1480],{"class":64}," is left over from an earlier run\"\n",[54,1482,1483,1485,1487,1490,1493,1495,1497],{"class":56,"line":172},[54,1484,384],{"class":121},[54,1486,387],{"class":113},[54,1488,1489],{"class":121}," load_workbook(p, ",[54,1491,1492],{"class":614},"read_only",[54,1494,387],{"class":113},[54,1496,887],{"class":117},[54,1498,193],{"class":121},[54,1500,1501,1503],{"class":56,"line":187},[54,1502,134],{"class":113},[54,1504,137],{"class":121},[54,1506,1507,1510,1512,1515,1518,1521,1523],{"class":56,"line":423},[54,1508,1509],{"class":121},"        missing ",[54,1511,387],{"class":113},[54,1513,1514],{"class":117}," set",[54,1516,1517],{"class":121},"(sheets) ",[54,1519,1520],{"class":113},"-",[54,1522,1514],{"class":117},[54,1524,1525],{"class":121},"(wb.sheetnames)\n",[54,1527,1528,1531,1533,1536,1538,1540,1542,1544,1546,1549,1551,1554,1556],{"class":56,"line":429},[54,1529,1530],{"class":113},"        assert",[54,1532,146],{"class":113},[54,1534,1535],{"class":121}," missing, ",[54,1537,809],{"class":113},[54,1539,812],{"class":64},[54,1541,816],{"class":815},[54,1543,10],{"class":121},[54,1545,821],{"class":815},[54,1547,1548],{"class":64}," is missing sheets ",[54,1550,816],{"class":815},[54,1552,1553],{"class":121},"missing",[54,1555,821],{"class":815},[54,1557,1558],{"class":64},"\"\n",[54,1560,1561,1564,1567,1569],{"class":56,"line":435},[54,1562,1563],{"class":113},"        for",[54,1565,1566],{"class":121}," name ",[54,1568,740],{"class":113},[54,1570,1571],{"class":121}," sheets:\n",[54,1573,1575,1578,1580,1583,1585,1588,1591,1594,1596,1599,1602,1604,1606],{"class":56,"line":1574},13,[54,1576,1577],{"class":121},"            rows ",[54,1579,387],{"class":113},[54,1581,1582],{"class":117}," sum",[54,1584,181],{"class":121},[54,1586,1587],{"class":117},"1",[54,1589,1590],{"class":113}," for",[54,1592,1593],{"class":121}," _ ",[54,1595,740],{"class":113},[54,1597,1598],{"class":121}," wb[name].iter_rows(",[54,1600,1601],{"class":614},"values_only",[54,1603,387],{"class":113},[54,1605,887],{"class":117},[54,1607,451],{"class":121},[54,1609,1611,1614,1617,1619,1622,1624,1626,1628,1630,1632,1634,1636,1639,1641,1644,1646,1649,1651],{"class":56,"line":1610},14,[54,1612,1613],{"class":113},"            assert",[54,1615,1616],{"class":121}," rows ",[54,1618,1464],{"class":113},[54,1620,1621],{"class":121}," min_rows, ",[54,1623,809],{"class":113},[54,1625,812],{"class":64},[54,1627,816],{"class":815},[54,1629,10],{"class":121},[54,1631,821],{"class":815},[54,1633,578],{"class":64},[54,1635,816],{"class":815},[54,1637,1638],{"class":121},"name",[54,1640,821],{"class":815},[54,1642,1643],{"class":64}," has only ",[54,1645,816],{"class":815},[54,1647,1648],{"class":121},"rows",[54,1650,821],{"class":815},[54,1652,1653],{"class":64}," rows\"\n",[54,1655,1657,1660],{"class":56,"line":1656},15,[54,1658,1659],{"class":113},"    finally",[54,1661,137],{"class":121},[54,1663,1665],{"class":56,"line":1664},16,[54,1666,787],{"class":121},[54,1668,1670,1673],{"class":56,"line":1669},17,[54,1671,1672],{"class":113},"    return",[54,1674,1675],{"class":64}," \"ok\"\n",[54,1677,1679],{"class":56,"line":1678},18,[54,1680,368],{"emptyLinePlaceholder":367},[54,1682,1684,1687,1689,1692,1694],{"class":56,"line":1683},19,[54,1685,1686],{"class":121},"started ",[54,1688,387],{"class":113},[54,1690,1691],{"class":121}," time.time() ",[54,1693,1520],{"class":113},[54,1695,1696],{"class":117}," 1\n",[54,1698,1700,1702,1704,1707,1709,1711],{"class":56,"line":1699},20,[54,1701,466],{"class":113},[54,1703,390],{"class":121},[54,1705,1706],{"class":64},"\"checked.xlsx\"",[54,1708,487],{"class":121},[54,1710,490],{"class":113},[54,1712,493],{"class":121},[54,1714,1716,1718,1720,1722,1724],{"class":56,"line":1715},21,[54,1717,498],{"class":121},[54,1719,387],{"class":113},[54,1721,503],{"class":121},[54,1723,409],{"class":64},[54,1725,193],{"class":121},[54,1727,1729,1731,1733,1735,1737,1739,1741],{"class":56,"line":1728},22,[54,1730,512],{"class":121},[54,1732,403],{"class":64},[54,1734,517],{"class":121},[54,1736,520],{"class":64},[54,1738,406],{"class":121},[54,1740,525],{"class":64},[54,1742,528],{"class":121},[54,1744,1746,1748,1750,1752,1754,1756,1758],{"class":56,"line":1745},23,[54,1747,512],{"class":121},[54,1749,535],{"class":64},[54,1751,517],{"class":121},[54,1753,540],{"class":64},[54,1755,406],{"class":121},[54,1757,545],{"class":117},[54,1759,528],{"class":121},[54,1761,1763,1765,1768,1770,1773,1775],{"class":56,"line":1762},24,[54,1764,438],{"class":117},[54,1766,1767],{"class":121},"(check_report(",[54,1769,1706],{"class":64},[54,1771,1772],{"class":121},", started, [",[54,1774,409],{"class":64},[54,1776,1777],{"class":121},"]))\n",[54,1779,1781,1783],{"class":56,"line":1780},25,[54,1782,1115],{"class":113},[54,1784,137],{"class":121},[54,1786,1788,1791,1793,1795,1797],{"class":56,"line":1787},26,[54,1789,1790],{"class":121},"    check_report(",[54,1792,1127],{"class":64},[54,1794,1772],{"class":121},[54,1796,409],{"class":64},[54,1798,528],{"class":121},[54,1800,1802,1804,1807,1809],{"class":56,"line":1801},27,[54,1803,1176],{"class":113},[54,1805,1806],{"class":117}," AssertionError",[54,1808,1181],{"class":113},[54,1810,799],{"class":121},[54,1812,1814,1816,1818,1821],{"class":56,"line":1813},28,[54,1815,1188],{"class":117},[54,1817,181],{"class":121},[54,1819,1820],{"class":64},"\"blocked:\"",[54,1822,1823],{"class":121},", e)\n",[10,1825,1826],{},"The modification-time check is what catches the most embarrassing failure: a job whose workbook was never closed, sending the file an earlier run left behind. The sheet and row checks catch the half-built file from Step 4, which here does not even have a Sales sheet.",[224,1828,233,1833,233,1836,233,1839,233,1842,233,1846,233,1852,233,1855,233,1859,233,1863,233,1866,233,1869,233,1877,233,1882,233,1886,233,1891,233,1895,233,1899,233,1902,233,1905,233,1909,233,1912,233,1915,233,1920,233,1924,233,1927,233,1930,233,1933,233,1937,233,1939,233,1942,233,1948,233,1951,233,1954,233,1956,233,1959,233,1963,233,1966,233,1969,233,1975,233,1978],{"viewBox":1829,"role":227,"ariaLabelledBy":1830,"xmlns":231,"style":232},"4 0 752 272",[1831,1832],"xd-symptom-t","xd-symptom-d",[235,1834,1835],{"id":1831},"From symptom to cause",[239,1837,1838],{"id":1832},"A decision diagram. The destructor message means an old xlsxwriter and a missing close. No file at all means close was never called. FileCreateError means the directory, permissions or a file open in Excel. A file with missing data means an error inside the with block.",[243,1840],{"x":245,"y":246,"width":247,"height":1841,"fill":249},"272",[243,1843],{"x":1844,"y":253,"width":1845,"height":967,"rx":953,"fill":997,"stroke":988,"style":259},"240.0","280.0",[261,1847,1849],{"transform":1848},"translate(368.0,29.0) scale(1.000)",[265,1850],{"d":1851,"fill":268,"stroke":1004,"style":963},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5",[19,1853,1854],{"x":1029,"y":967,"style":1008},"What do you see?",[56,1856],{"x1":1029,"y1":1857,"x2":1029,"y2":1858,"stroke":988,"style":259},"91.5","111.5",[56,1860],{"x1":1861,"y1":1858,"x2":1862,"y2":1858,"stroke":988,"style":259},"104.0","656.0",[56,1864],{"x1":1861,"y1":1858,"x2":1861,"y2":1865,"stroke":988,"style":259},"157.5",[990,1867],{"points":1868,"fill":993},"104.0,161.5 99.0,152.5 109.0,152.5",[243,1870],{"x":1871,"y":1872,"width":1873,"height":1874,"rx":1875,"fill":249,"stroke":955,"style":1876},"35.6","121.5","136.7","21.0","10","stroke-width:1.2px",[19,1878,1881],{"x":1861,"y":1879,"style":1880},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","destructor message",[243,1883],{"x":252,"y":1884,"width":1885,"height":972,"rx":953,"fill":257,"stroke":258,"style":259},"163.5","168.0",[261,1887,1889],{"transform":1888},"translate(92.0,176.5) scale(1.000)",[265,1890],{"d":267,"fill":268,"stroke":269,"style":963},[19,1892,1894],{"x":1861,"y":1893,"style":1031},"223.0","Old xlsxwriter",[19,1896,1898],{"x":1861,"y":1897,"style":977},"242.5","add close(), upgrade",[56,1900],{"x1":1901,"y1":1858,"x2":1901,"y2":1865,"stroke":988,"style":259},"288.0",[990,1903],{"points":1904,"fill":993},"288.0,161.5 283.0,152.5 293.0,152.5",[243,1906],{"x":1907,"y":1872,"width":1908,"height":1874,"rx":1875,"fill":249,"stroke":955,"style":1876},"260.8","54.3",[19,1910,1911],{"x":1901,"y":1879,"style":1880},"no file",[243,1913],{"x":1914,"y":1884,"width":1885,"height":972,"rx":953,"fill":299,"stroke":300,"style":259},"204.0",[261,1916,1918],{"transform":1917},"translate(276.0,176.5) scale(1.000)",[265,1919],{"d":306,"fill":268,"stroke":307,"style":963},[19,1921,1923],{"x":1901,"y":1893,"style":1922},"font-size:13.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","close() never ran",[19,1925,1926],{"x":1901,"y":1897,"style":977},"use with",[56,1928],{"x1":1929,"y1":1858,"x2":1929,"y2":1865,"stroke":988,"style":259},"472.0",[990,1931],{"points":1932,"fill":993},"472.0,161.5 467.0,152.5 477.0,152.5",[243,1934],{"x":1935,"y":1872,"width":1936,"height":1874,"rx":1875,"fill":249,"stroke":955,"style":1876},"418.0","108.0",[19,1938,1036],{"x":1929,"y":1879,"style":1880},[243,1940],{"x":1941,"y":1884,"width":1885,"height":972,"rx":953,"fill":997,"stroke":988,"style":259},"388.0",[261,1943,1945],{"transform":1944},"translate(460.0,176.5) scale(1.000)",[265,1946],{"d":1947,"fill":268,"stroke":1004,"style":963},"M3 6h6l2 2h10v11H3z",[19,1949,1950],{"x":1929,"y":1893,"style":1008},"Path problem",[19,1952,1953],{"x":1929,"y":1897,"style":977},"dir, permission, lock",[56,1955],{"x1":1862,"y1":1858,"x2":1862,"y2":1865,"stroke":988,"style":259},[990,1957],{"points":1958,"fill":993},"656.0,161.5 651.0,152.5 661.0,152.5",[243,1960],{"x":1961,"y":1872,"width":1962,"height":1874,"rx":1875,"fill":249,"stroke":955,"style":1876},"603.2","105.7",[19,1964,1965],{"x":1862,"y":1879,"style":1880},"file but no data",[243,1967],{"x":1968,"y":1884,"width":1885,"height":972,"rx":953,"fill":1074,"stroke":1075,"style":259},"572.0",[261,1970,1972],{"transform":1971},"translate(644.0,176.5) scale(1.000)",[265,1973],{"d":1974,"fill":268,"stroke":1082,"style":963},"M6 3h9l4 4v14H6z M15 3v4h4",[19,1976,1977],{"x":1862,"y":1893,"style":1086},"Error mid-build",[19,1979,1980],{"x":1862,"y":1897,"style":977},"temp file + rename",[42,1982,1984],{"id":1983},"common-pitfalls","Common pitfalls",[1986,1987,1988,1996,2005,2019,2033,2042],"ul",{},[1989,1990,1991,1995],"li",{},[1992,1993,1994],"strong",{},"Relying on garbage collection."," Nothing writes an unclosed workbook in current xlsxwriter; always close.",[1989,1997,1998,2004],{},[1992,1999,2000,2001,2003],{},"Calling ",[22,2002,33],{}," twice."," The second call only warns \"Calling close() on already closed file\"; harmless, but a sign that close is in two code paths.",[1989,2006,2007,2013,2014,575,2017,40],{},[1992,2008,2009,2012],{},[22,2010,2011],{},"writer.save()"," in pandas."," Removed in pandas 2.0; use ",[22,2015,2016],{},"with pd.ExcelWriter(...)",[22,2018,33],{},[1989,2020,2021,2026,2027,2029,2030,2032],{},[1992,2022,2023,2024,40],{},"Catching everything around ",[22,2025,33],{}," A bare ",[22,2028,1176],{}," hides ",[22,2031,1036],{},", which is exactly the error you need to see.",[1989,2034,2035,2038,2039,2041],{},[1992,2036,2037],{},"Partial files."," A ",[22,2040,466],{}," block writes the file even after an error; write to a temporary name and rename on success.",[1989,2043,2044,2047],{},[1992,2045,2046],{},"Jupyter notebooks."," A workbook created in one cell and never closed stays in memory until the kernel restarts; the file never appears.",[42,2049,2051],{"id":2050},"conclusion","Conclusion",[10,2053,2054,2055,2057,2058,2060,2061,2063],{},"\"Exception caught in workbook destructor\" is old xlsxwriter's way of saying the workbook was never explicitly closed and the automatic save failed; newer versions say nothing and write nothing. Close every workbook with a ",[22,2056,466],{}," block or ",[22,2059,33],{},", catch ",[22,2062,1036],{}," where the file is created, create output directories first, and write to a temporary name that is renamed only when the whole report has been built.",[42,2065,2067],{"id":2066},"frequently-asked-questions","Frequently asked questions",[10,2069,2070,2073],{},[1992,2071,2072],{},"What does \"Explicit close() may be required for workbook\" mean?","\nThe workbook was never closed, so xlsxwriter versions up to 1.0 tried to save it when the object was garbage collected, and that save failed. Call close() yourself, or use a with block, and the real error will be raised where you can see it.",[10,2075,2076,2079],{},[1992,2077,2078],{},"Why does xlsxwriter not create my file?","\nThe file is only written when close() runs. If the code never calls it, current versions of xlsxwriter write nothing and report nothing.",[10,2081,2082,2085],{},[1992,2083,2084],{},"What is FileCreateError in xlsxwriter?","\nThe exception close() raises when it cannot create the output file: the directory does not exist, there is no write permission, or on Windows the file is open in Excel.",[10,2087,2088,2091],{},[1992,2089,2090],{},"How do I save a pandas ExcelWriter now that save() is gone?","\nUse it as a context manager, with pd.ExcelWriter(path) as writer:, or call writer.close() when finished.",[42,2093,2095],{"id":2094},"related","Related",[1986,2097,2098,2104,2111,2118,2125],{},[1989,2099,2100,2101,2103],{},"Up one level: ",[36,2102,39],{"href":38}," — the other common errors.",[1989,2105,2106,2110],{},[36,2107,2109],{"href":2108},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excelwriter-has-no-attribute-save\u002F","Fix \"ExcelWriter object has no attribute save\" in pandas"," — the pandas side of closing.",[1989,2112,2113,2117],{},[36,2114,2116],{"href":2115},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-excel-repaired-content-after-writing-with-python\u002F","Fix \"Excel Found Unreadable Content\" After Writing with Python"," — when the file is written but broken.",[1989,2119,2120,2124],{},[36,2121,2123],{"href":2122},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder\u002F","Ignore Excel Lock Files When Processing a Folder"," — the other side of files open in Excel.",[1989,2126,2127,2131],{},[36,2128,2130],{"href":2129},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run\u002F","Detect a Scheduled Report That Did Not Run"," — catching jobs that silently produce nothing.",[2133,2134,2135],"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":20,"searchDepth":110,"depth":110,"links":2137},[2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148],{"id":44,"depth":110,"text":45},{"id":84,"depth":110,"text":85},{"id":328,"depth":110,"text":329},{"id":457,"depth":110,"text":458},{"id":696,"depth":110,"text":697},{"id":1096,"depth":110,"text":1097},{"id":1373,"depth":110,"text":1374},{"id":1983,"depth":110,"text":1984},{"id":2050,"depth":110,"text":2051},{"id":2066,"depth":110,"text":2067},{"id":2094,"depth":110,"text":2095},"2026-09-29","The xlsxwriter message \"Exception caught in workbook destructor. Explicit close() may be required\" comes from versions before 1.1, where an unclosed workbook was saved when garbage collected. Current versions print nothing and write nothing instead. Close every workbook explicitly, handle FileCreateError, and avoid writing half-built reports from a with block.","md",[2153,2155,2157,2159],{"q":2072,"a":2154},"The workbook was never closed, so xlsxwriter versions up to 1.0 tried to save it when the object was garbage collected, and that save failed. Call close() yourself, or use a with block, and the real error will be raised where you can see it.",{"q":2078,"a":2156},"The file is only written when close() runs. If the code never calls it, current versions of xlsxwriter write nothing and report nothing.",{"q":2084,"a":2158},"The exception close() raises when it cannot create the output file: the directory does not exist, there is no write permission, or on Windows the file is open in Excel.",{"q":2090,"a":2160},"Use it as a context manager, with pd.ExcelWriter(path) as writer:, or call writer.close() when finished.","fix xlsxwriter exception caught in workbook destructor",null,{"breadcrumb":2164},[2165,2168,2171],{"name":2166,"item":2167},"Home","\u002F",{"name":2169,"item":2170},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":39,"item":38},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor",{"title":2174,"description":2175},"Fix xlsxwriter: Exception caught in workbook destructor","What 'Exception caught in workbook destructor. Explicit close() may be required for workbook' means in xlsxwriter, why new versions write no file at all when close() is missing, FileCreateError, pandas ExcelWriter.save, and partial files.","fix-xlsxwriter-exception-caught-in-workbook-destructor","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-exception-caught-in-workbook-destructor\u002Findex",[2179,2180],"troubleshooting","xlsxwriter","how-to","zeP1gmhpQQxW_Tgri6acvpQ6bUUi0XlfielQzg-I9BM",[2184,2188],{"title":2185,"path":2186,"stem":2187,"children":-1},"Fix \"XLRDError: Excel xlsx file; not supported\" in Python","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlrderror-excel-xlsx-file-not-supported","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlrderror-excel-xlsx-file-not-supported\u002Findex",{"title":2189,"path":2190,"stem":2191,"children":-1},"Fix XlsxWriter Overwriting an Existing Workbook","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-overwriting-an-existing-workbook","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-xlsxwriter-overwriting-an-existing-workbook\u002Findex",[2193,2195],{"path":2194,"title":2169},"\u002Fgetting-started-with-python-excel-automation",{"path":2196,"title":39},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[2199,2203,2206,2209,2212,2215],{"path":2200,"title":2201,"kind":2202},"\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":2204,"title":2205,"kind":2181},"\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":2207,"title":2208,"kind":2202},"\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":2210,"title":2211,"kind":2181},"\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":2213,"title":2214,"kind":2181},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":2216,"title":2217,"kind":2218},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972067809]