[{"data":1,"prerenderedAt":2294},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python":3,"surround:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python":2260,"ancestors:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python":2268,"children:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python":2273,"related:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python":2274},{"id":4,"title":5,"body":6,"dateModified":2226,"datePublished":2226,"description":2227,"extension":2228,"faq":2229,"keyword":2238,"kind":2239,"meta":2240,"navigation":126,"path":2249,"seo":2250,"slug":2253,"stem":2254,"topics":2255,"type":2258,"__hash__":2259},"docs\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python\u002Findex.md","Write Excel Files Atomically in Python",{"type":7,"value":8,"toc":2213},"minimark",[9,28,33,61,79,83,90,594,597,700,704,715,1076,1097,1101,1104,1316,1324,1328,1331,1481,1491,1621,1625,1628,1925,1932,1936,1979,2089,2093,2135,2139,2148,2152,2158,2164,2170,2176,2180,2209],[10,11,12,13,17,18,21,22,27],"p",{},"A report job saves ",[14,15,16],"code",{},"report.xlsx"," every hour into a shared folder that a dashboard, a colleague's macro and a sync client all read. One day the job runs out of memory halfway through ",[14,19,20],{},"wb.save()",". What is left on disk is not last hour's report and not this hour's — it is a truncated zip file that Excel calls corrupt, and the dashboard fails until someone notices. Even without a crash, a reader that opens the file during the second or two the save takes can get half a workbook. The fix is an old Unix habit: write the new file under a temporary name next to the target, make sure it is complete and valid, then swap it into place with one atomic rename. Readers see either the old file or the new one, never something in between. This page builds that as a context manager and tests that it holds up. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[14,41,42],{"__ignoreMap":39},[43,44,47,51,55,58],"span",{"class":45,"line":46},"line",1,[43,48,50],{"class":49},"sMTad","pip",[43,52,54],{"class":53},"srMev"," install",[43,56,57],{"class":53}," openpyxl",[43,59,60],{"class":53}," pandas\n",[10,62,63,64,67,68,67,71,74,75,78],{},"Everything here uses the standard library (",[14,65,66],{},"os",", ",[14,69,70],{},"tempfile",[14,72,73],{},"zipfile",") plus the Excel library you already use. The behaviour of ",[14,76,77],{},"os.replace"," described below holds on Linux, macOS and Windows for files on the same local volume; network shares and sync folders are discussed at the end.",[29,80,82],{"id":81},"step-1-see-the-problem","Step 1 — See the problem",[10,84,85,86,89],{},"Simulate a save that fails partway through. ",[14,87,88],{},"openpyxl"," writes the zip archive piece by piece, so an exception in the middle leaves a broken file behind:",[34,91,95],{"className":92,"code":93,"language":94,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import zipfile\nfrom openpyxl import Workbook, load_workbook\n\ndef make_workbook(label, rows=2000):\n    wb = Workbook()\n    ws = wb.active\n    ws.append([\"Run\", \"Row\", \"Value\"])\n    for i in range(rows):\n        ws.append([label, i, i * 1.5])\n    return wb\n\nmake_workbook(\"good run\").save(\"report.xlsx\")              # yesterday's good report\n\nclass Boom(Exception):\n    pass\n\nwb = make_workbook(\"new run\")\noriginal = zipfile.ZipFile.writestr                        # simulate dying mid-save\ncalls = {\"n\": 0}\ndef flaky(self, *a, **k):\n    calls[\"n\"] += 1\n    if calls[\"n\"] == 4:\n        raise Boom(\"out of memory\")\n    return original(self, *a, **k)\nzipfile.ZipFile.writestr = flaky\ntry:\n    wb.save(\"report.xlsx\")\nexcept Boom as e:\n    print(\"save failed:\", e)\nfinally:\n    zipfile.ZipFile.writestr = original\n\ntry:\n    load_workbook(\"report.xlsx\")\n    print(\"report.xlsx still opens\")\nexcept Exception as e:\n    print(\"report.xlsx is now broken:\", type(e).__name__)\n","python",[14,96,97,107,121,128,151,162,173,195,213,227,236,241,263,268,285,291,296,313,327,350,372,389,411,425,447,458,466,476,491,505,513,524,529,536,546,558,571],{"__ignoreMap":39},[43,98,99,103],{"class":45,"line":46},[43,100,102],{"class":101},"s-kum","import",[43,104,106],{"class":105},"skGVy"," zipfile\n",[43,108,110,113,116,118],{"class":45,"line":109},2,[43,111,112],{"class":101},"from",[43,114,115],{"class":105}," openpyxl ",[43,117,102],{"class":101},[43,119,120],{"class":105}," Workbook, load_workbook\n",[43,122,124],{"class":45,"line":123},3,[43,125,127],{"emptyLinePlaceholder":126},true,"\n",[43,129,131,134,138,141,144,148],{"class":45,"line":130},4,[43,132,133],{"class":101},"def",[43,135,137],{"class":136},"s_Opv"," make_workbook",[43,139,140],{"class":105},"(label, rows",[43,142,143],{"class":101},"=",[43,145,147],{"class":146},"sP0c6","2000",[43,149,150],{"class":105},"):\n",[43,152,154,157,159],{"class":45,"line":153},5,[43,155,156],{"class":105},"    wb ",[43,158,143],{"class":101},[43,160,161],{"class":105}," Workbook()\n",[43,163,165,168,170],{"class":45,"line":164},6,[43,166,167],{"class":105},"    ws ",[43,169,143],{"class":101},[43,171,172],{"class":105}," wb.active\n",[43,174,176,179,182,184,187,189,192],{"class":45,"line":175},7,[43,177,178],{"class":105},"    ws.append([",[43,180,181],{"class":53},"\"Run\"",[43,183,67],{"class":105},[43,185,186],{"class":53},"\"Row\"",[43,188,67],{"class":105},[43,190,191],{"class":53},"\"Value\"",[43,193,194],{"class":105},"])\n",[43,196,198,201,204,207,210],{"class":45,"line":197},8,[43,199,200],{"class":101},"    for",[43,202,203],{"class":105}," i ",[43,205,206],{"class":101},"in",[43,208,209],{"class":146}," range",[43,211,212],{"class":105},"(rows):\n",[43,214,216,219,222,225],{"class":45,"line":215},9,[43,217,218],{"class":105},"        ws.append([label, i, i ",[43,220,221],{"class":101},"*",[43,223,224],{"class":146}," 1.5",[43,226,194],{"class":105},[43,228,230,233],{"class":45,"line":229},10,[43,231,232],{"class":101},"    return",[43,234,235],{"class":105}," wb\n",[43,237,239],{"class":45,"line":238},11,[43,240,127],{"emptyLinePlaceholder":126},[43,242,244,247,250,253,256,259],{"class":45,"line":243},12,[43,245,246],{"class":105},"make_workbook(",[43,248,249],{"class":53},"\"good run\"",[43,251,252],{"class":105},").save(",[43,254,255],{"class":53},"\"report.xlsx\"",[43,257,258],{"class":105},")              ",[43,260,262],{"class":261},"s-wDw","# yesterday's good report\n",[43,264,266],{"class":45,"line":265},13,[43,267,127],{"emptyLinePlaceholder":126},[43,269,271,274,277,280,283],{"class":45,"line":270},14,[43,272,273],{"class":101},"class",[43,275,276],{"class":49}," Boom",[43,278,279],{"class":105},"(",[43,281,282],{"class":146},"Exception",[43,284,150],{"class":105},[43,286,288],{"class":45,"line":287},15,[43,289,290],{"class":101},"    pass\n",[43,292,294],{"class":45,"line":293},16,[43,295,127],{"emptyLinePlaceholder":126},[43,297,299,302,304,307,310],{"class":45,"line":298},17,[43,300,301],{"class":105},"wb ",[43,303,143],{"class":101},[43,305,306],{"class":105}," make_workbook(",[43,308,309],{"class":53},"\"new run\"",[43,311,312],{"class":105},")\n",[43,314,316,319,321,324],{"class":45,"line":315},18,[43,317,318],{"class":105},"original ",[43,320,143],{"class":101},[43,322,323],{"class":105}," zipfile.ZipFile.writestr                        ",[43,325,326],{"class":261},"# simulate dying mid-save\n",[43,328,330,333,335,338,341,344,347],{"class":45,"line":329},19,[43,331,332],{"class":105},"calls ",[43,334,143],{"class":101},[43,336,337],{"class":105}," {",[43,339,340],{"class":53},"\"n\"",[43,342,343],{"class":105},": ",[43,345,346],{"class":146},"0",[43,348,349],{"class":105},"}\n",[43,351,353,355,358,361,363,366,369],{"class":45,"line":352},20,[43,354,133],{"class":101},[43,356,357],{"class":136}," flaky",[43,359,360],{"class":105},"(self, ",[43,362,221],{"class":101},[43,364,365],{"class":105},"a, ",[43,367,368],{"class":101},"**",[43,370,371],{"class":105},"k):\n",[43,373,375,378,380,383,386],{"class":45,"line":374},21,[43,376,377],{"class":105},"    calls[",[43,379,340],{"class":53},[43,381,382],{"class":105},"] ",[43,384,385],{"class":101},"+=",[43,387,388],{"class":146}," 1\n",[43,390,392,395,398,400,402,405,408],{"class":45,"line":391},22,[43,393,394],{"class":101},"    if",[43,396,397],{"class":105}," calls[",[43,399,340],{"class":53},[43,401,382],{"class":105},[43,403,404],{"class":101},"==",[43,406,407],{"class":146}," 4",[43,409,410],{"class":105},":\n",[43,412,414,417,420,423],{"class":45,"line":413},23,[43,415,416],{"class":101},"        raise",[43,418,419],{"class":105}," Boom(",[43,421,422],{"class":53},"\"out of memory\"",[43,424,312],{"class":105},[43,426,428,430,433,436,438,440,442,444],{"class":45,"line":427},24,[43,429,232],{"class":101},[43,431,432],{"class":105}," original(",[43,434,435],{"class":146},"self",[43,437,67],{"class":105},[43,439,221],{"class":101},[43,441,365],{"class":105},[43,443,368],{"class":101},[43,445,446],{"class":105},"k)\n",[43,448,450,453,455],{"class":45,"line":449},25,[43,451,452],{"class":105},"zipfile.ZipFile.writestr ",[43,454,143],{"class":101},[43,456,457],{"class":105}," flaky\n",[43,459,461,464],{"class":45,"line":460},26,[43,462,463],{"class":101},"try",[43,465,410],{"class":105},[43,467,469,472,474],{"class":45,"line":468},27,[43,470,471],{"class":105},"    wb.save(",[43,473,255],{"class":53},[43,475,312],{"class":105},[43,477,479,482,485,488],{"class":45,"line":478},28,[43,480,481],{"class":101},"except",[43,483,484],{"class":105}," Boom ",[43,486,487],{"class":101},"as",[43,489,490],{"class":105}," e:\n",[43,492,494,497,499,502],{"class":45,"line":493},29,[43,495,496],{"class":146},"    print",[43,498,279],{"class":105},[43,500,501],{"class":53},"\"save failed:\"",[43,503,504],{"class":105},", e)\n",[43,506,508,511],{"class":45,"line":507},30,[43,509,510],{"class":101},"finally",[43,512,410],{"class":105},[43,514,516,519,521],{"class":45,"line":515},31,[43,517,518],{"class":105},"    zipfile.ZipFile.writestr ",[43,520,143],{"class":101},[43,522,523],{"class":105}," original\n",[43,525,527],{"class":45,"line":526},32,[43,528,127],{"emptyLinePlaceholder":126},[43,530,532,534],{"class":45,"line":531},33,[43,533,463],{"class":101},[43,535,410],{"class":105},[43,537,539,542,544],{"class":45,"line":538},34,[43,540,541],{"class":105},"    load_workbook(",[43,543,255],{"class":53},[43,545,312],{"class":105},[43,547,549,551,553,556],{"class":45,"line":548},35,[43,550,496],{"class":146},[43,552,279],{"class":105},[43,554,555],{"class":53},"\"report.xlsx still opens\"",[43,557,312],{"class":105},[43,559,561,563,566,569],{"class":45,"line":560},36,[43,562,481],{"class":101},[43,564,565],{"class":146}," Exception",[43,567,568],{"class":101}," as",[43,570,490],{"class":105},[43,572,574,576,578,581,583,586,589,592],{"class":45,"line":573},37,[43,575,496],{"class":146},[43,577,279],{"class":105},[43,579,580],{"class":53},"\"report.xlsx is now broken:\"",[43,582,67],{"class":105},[43,584,585],{"class":146},"type",[43,587,588],{"class":105},"(e).",[43,590,591],{"class":146},"__name__",[43,593,312],{"class":105},[10,595,596],{},"Saving straight over the target truncated it the moment the save began, so when the save died, yesterday's good report was already gone and nothing valid replaced it. The same window exists on every successful save too — it is just short enough that it rarely bites, until a reader polls at the wrong moment.",[598,599,607,608,607,612,607,616,607,623,607,633,607,644,607,651,607,657,607,662,607,666,607,670,607,675,607,682,607,687,607,691,607,694,607,697],"svg",{"viewBox":600,"role":601,"ariaLabelledBy":602,"xmlns":605,"style":606},"4 0 752 207","img",[603,604],"at-direct-t","at-direct-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  ",[609,610,611],"title",{"id":603},"Saving in place versus writing then swapping",[613,614,615],"desc",{"id":604},"Saving directly truncates the target first, so a crash or a reader during the save meets a broken file. Writing to a temporary file and renaming it over the target means the target is always either the complete old file or the complete new one.",[617,618],"rect",{"x":619,"y":346,"width":620,"height":621,"fill":622},"4","752","207","#ffffff",[617,624],{"x":625,"y":626,"width":627,"height":628,"rx":629,"fill":630,"stroke":631,"style":632},"20.0","16.0","351.0","175.0","14","#fce9e9","var(--danger,#dc2626)","stroke-width:2px",[634,635,637],"g",{"transform":636},"translate(182.5,29.0) scale(1.083)",[638,639],"path",{"d":640,"fill":641,"stroke":642,"style":643},"M6 6l12 12 M18 6L6 18","none","var(--accent-ink,#be185d)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[645,646,650],"text",{"x":647,"y":648,"style":649},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","save over report.xlsx",[45,652],{"x1":653,"y1":654,"x2":655,"y2":654,"stroke":631,"style":656},"36.0","91.0","355.0","stroke-width:1px",[645,658,661],{"x":647,"y":659,"style":660},"111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","target truncated at once",[645,663,665],{"x":647,"y":664,"style":660},"137.0","crash leaves a broken file",[645,667,669],{"x":647,"y":668,"style":660},"163.0","readers can see half a file",[617,671],{"x":672,"y":626,"width":627,"height":628,"rx":629,"fill":673,"stroke":674,"style":632},"389.0","#d9f4f1","var(--teal,#0f9488)",[634,676,678],{"transform":677},"translate(551.5,29.0) scale(1.083)",[638,679],{"d":680,"fill":641,"stroke":681,"style":643},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[645,683,686],{"x":684,"y":648,"style":685},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","temp file + os.replace",[45,688],{"x1":689,"y1":654,"x2":690,"y2":654,"stroke":674,"style":656},"405.0","724.0",[645,692,693],{"x":684,"y":659,"style":660},"target untouched while writing",[645,695,696],{"x":684,"y":664,"style":660},"crash leaves the old file",[645,698,699],{"x":684,"y":668,"style":660},"readers see old or new, never partial",[29,701,703],{"id":702},"step-2-the-atomic-write-context-manager","Step 2 — The atomic write context manager",[10,705,706,707,711,712,714],{},"The pattern has four parts: create a temporary file ",[708,709,710],"em",{},"in the same directory"," as the target, write the workbook to it, flush it to disk, and ",[14,713,77],{}," it over the target. If anything fails, delete the temporary file and leave the target alone:",[34,716,718],{"className":92,"code":717,"language":94,"meta":39,"style":39},"import os\nimport tempfile\nfrom contextlib import contextmanager\nfrom pathlib import Path\n\n@contextmanager\ndef atomic_path(target, validate=None):\n    \"\"\"Yield a temporary path next to `target`; replace `target` with it on success.\"\"\"\n    target = Path(target)\n    fd, tmp = tempfile.mkstemp(prefix=f\".{target.stem}-\", suffix=target.suffix, dir=target.parent)\n    os.close(fd)\n    tmp = Path(tmp)\n    try:\n        yield tmp\n        with open(tmp, \"rb+\") as fh:                     # make sure the bytes are on disk\n            fh.flush()\n            os.fsync(fh.fileno())\n        if validate:\n            validate(tmp)\n        if target.exists():\n            os.chmod(tmp, target.stat().st_mode & 0o777)  # keep the old file's permissions\n        os.replace(tmp, target)\n    except BaseException:\n        tmp.unlink(missing_ok=True)\n        raise\n\nwith atomic_path(\"report.xlsx\") as tmp:\n    make_workbook(\"good run\").save(tmp)\nprint(\"report.xlsx opens:\", load_workbook(\"report.xlsx\").active[\"A2\"].value)\nprint(\"leftover temp files:\", list(Path(\".\").glob(\".report-*\")))\n",[14,719,720,727,734,746,758,762,767,784,789,799,852,857,867,874,882,907,912,917,925,930,937,957,962,972,987,992,996,1013,1023,1047],{"__ignoreMap":39},[43,721,722,724],{"class":45,"line":46},[43,723,102],{"class":101},[43,725,726],{"class":105}," os\n",[43,728,729,731],{"class":45,"line":109},[43,730,102],{"class":101},[43,732,733],{"class":105}," tempfile\n",[43,735,736,738,741,743],{"class":45,"line":123},[43,737,112],{"class":101},[43,739,740],{"class":105}," contextlib ",[43,742,102],{"class":101},[43,744,745],{"class":105}," contextmanager\n",[43,747,748,750,753,755],{"class":45,"line":130},[43,749,112],{"class":101},[43,751,752],{"class":105}," pathlib ",[43,754,102],{"class":101},[43,756,757],{"class":105}," Path\n",[43,759,760],{"class":45,"line":153},[43,761,127],{"emptyLinePlaceholder":126},[43,763,764],{"class":45,"line":164},[43,765,766],{"class":136},"@contextmanager\n",[43,768,769,771,774,777,779,782],{"class":45,"line":175},[43,770,133],{"class":101},[43,772,773],{"class":136}," atomic_path",[43,775,776],{"class":105},"(target, validate",[43,778,143],{"class":101},[43,780,781],{"class":146},"None",[43,783,150],{"class":105},[43,785,786],{"class":45,"line":197},[43,787,788],{"class":53},"    \"\"\"Yield a temporary path next to `target`; replace `target` with it on success.\"\"\"\n",[43,790,791,794,796],{"class":45,"line":215},[43,792,793],{"class":105},"    target ",[43,795,143],{"class":101},[43,797,798],{"class":105}," Path(target)\n",[43,800,801,804,806,809,813,815,818,821,825,828,831,834,836,839,841,844,847,849],{"class":45,"line":229},[43,802,803],{"class":105},"    fd, tmp ",[43,805,143],{"class":101},[43,807,808],{"class":105}," tempfile.mkstemp(",[43,810,812],{"class":811},"sa561","prefix",[43,814,143],{"class":101},[43,816,817],{"class":101},"f",[43,819,820],{"class":53},"\".",[43,822,824],{"class":823},"sSjpA","{",[43,826,827],{"class":105},"target.stem",[43,829,830],{"class":823},"}",[43,832,833],{"class":53},"-\"",[43,835,67],{"class":105},[43,837,838],{"class":811},"suffix",[43,840,143],{"class":101},[43,842,843],{"class":105},"target.suffix, ",[43,845,846],{"class":811},"dir",[43,848,143],{"class":101},[43,850,851],{"class":105},"target.parent)\n",[43,853,854],{"class":45,"line":238},[43,855,856],{"class":105},"    os.close(fd)\n",[43,858,859,862,864],{"class":45,"line":243},[43,860,861],{"class":105},"    tmp ",[43,863,143],{"class":101},[43,865,866],{"class":105}," Path(tmp)\n",[43,868,869,872],{"class":45,"line":265},[43,870,871],{"class":101},"    try",[43,873,410],{"class":105},[43,875,876,879],{"class":45,"line":270},[43,877,878],{"class":101},"        yield",[43,880,881],{"class":105}," tmp\n",[43,883,884,887,890,893,896,899,901,904],{"class":45,"line":287},[43,885,886],{"class":101},"        with",[43,888,889],{"class":146}," open",[43,891,892],{"class":105},"(tmp, ",[43,894,895],{"class":53},"\"rb+\"",[43,897,898],{"class":105},") ",[43,900,487],{"class":101},[43,902,903],{"class":105}," fh:                     ",[43,905,906],{"class":261},"# make sure the bytes are on disk\n",[43,908,909],{"class":45,"line":293},[43,910,911],{"class":105},"            fh.flush()\n",[43,913,914],{"class":45,"line":298},[43,915,916],{"class":105},"            os.fsync(fh.fileno())\n",[43,918,919,922],{"class":45,"line":315},[43,920,921],{"class":101},"        if",[43,923,924],{"class":105}," validate:\n",[43,926,927],{"class":45,"line":329},[43,928,929],{"class":105},"            validate(tmp)\n",[43,931,932,934],{"class":45,"line":352},[43,933,921],{"class":101},[43,935,936],{"class":105}," target.exists():\n",[43,938,939,942,945,948,951,954],{"class":45,"line":374},[43,940,941],{"class":105},"            os.chmod(tmp, target.stat().st_mode ",[43,943,944],{"class":101},"&",[43,946,947],{"class":101}," 0o",[43,949,950],{"class":146},"777",[43,952,953],{"class":105},")  ",[43,955,956],{"class":261},"# keep the old file's permissions\n",[43,958,959],{"class":45,"line":391},[43,960,961],{"class":105},"        os.replace(tmp, target)\n",[43,963,964,967,970],{"class":45,"line":413},[43,965,966],{"class":101},"    except",[43,968,969],{"class":146}," BaseException",[43,971,410],{"class":105},[43,973,974,977,980,982,985],{"class":45,"line":427},[43,975,976],{"class":105},"        tmp.unlink(",[43,978,979],{"class":811},"missing_ok",[43,981,143],{"class":101},[43,983,984],{"class":146},"True",[43,986,312],{"class":105},[43,988,989],{"class":45,"line":449},[43,990,991],{"class":101},"        raise\n",[43,993,994],{"class":45,"line":460},[43,995,127],{"emptyLinePlaceholder":126},[43,997,998,1001,1004,1006,1008,1010],{"class":45,"line":468},[43,999,1000],{"class":101},"with",[43,1002,1003],{"class":105}," atomic_path(",[43,1005,255],{"class":53},[43,1007,898],{"class":105},[43,1009,487],{"class":101},[43,1011,1012],{"class":105}," tmp:\n",[43,1014,1015,1018,1020],{"class":45,"line":478},[43,1016,1017],{"class":105},"    make_workbook(",[43,1019,249],{"class":53},[43,1021,1022],{"class":105},").save(tmp)\n",[43,1024,1025,1028,1030,1033,1036,1038,1041,1044],{"class":45,"line":493},[43,1026,1027],{"class":146},"print",[43,1029,279],{"class":105},[43,1031,1032],{"class":53},"\"report.xlsx opens:\"",[43,1034,1035],{"class":105},", load_workbook(",[43,1037,255],{"class":53},[43,1039,1040],{"class":105},").active[",[43,1042,1043],{"class":53},"\"A2\"",[43,1045,1046],{"class":105},"].value)\n",[43,1048,1049,1051,1053,1056,1058,1061,1064,1067,1070,1073],{"class":45,"line":507},[43,1050,1027],{"class":146},[43,1052,279],{"class":105},[43,1054,1055],{"class":53},"\"leftover temp files:\"",[43,1057,67],{"class":105},[43,1059,1060],{"class":146},"list",[43,1062,1063],{"class":105},"(Path(",[43,1065,1066],{"class":53},"\".\"",[43,1068,1069],{"class":105},").glob(",[43,1071,1072],{"class":53},"\".report-*\"",[43,1074,1075],{"class":105},")))\n",[10,1077,1078,1081,1082,1084,1085,1088,1089,1092,1093,1096],{},[14,1079,1080],{},"tempfile.mkstemp"," creates a uniquely named file, so two jobs writing the same report at once cannot clobber each other's temporary file. It must be in the same directory: ",[14,1083,77],{}," is only atomic within one file system, and the system temp directory is often on a different one — renaming across file systems either fails or turns into a non-atomic copy. The leading dot and the ",[14,1086,1087],{},".xlsx"," suffix matter too: the dot hides the file from casual directory listings and from many folder watchers, and the suffix lets Excel libraries that check extensions write to it. ",[14,1090,1091],{},"fsync"," asks the operating system to put the bytes on disk before the rename, so a power cut cannot leave a renamed but empty file. ",[14,1094,1095],{},"mkstemp"," creates the file readable only by its owner, so the old file's permissions are copied across — otherwise colleagues who could read yesterday's report suddenly cannot.",[29,1098,1100],{"id":1099},"step-3-validate-before-the-swap","Step 3 — Validate before the swap",[10,1102,1103],{},"Since nothing is visible until the rename, there is a free moment to check the new file. A validator that opens the workbook and checks it has data catches a broken or empty report before it replaces a good one:",[34,1105,1107],{"className":92,"code":1106,"language":94,"meta":39,"style":39},"def has_rows(path, sheet=None, min_rows=2):\n    wb = load_workbook(path, read_only=True)\n    ws = wb[sheet] if sheet else wb.active\n    n = ws.max_row\n    wb.close()\n    if n \u003C min_rows:\n        raise ValueError(f\"{path.name} has only {n} row(s)\")\n\ntry:\n    with atomic_path(\"report.xlsx\", validate=has_rows) as tmp:\n        Workbook().save(tmp)                          # an empty report, e.g. the query returned nothing\nexcept ValueError as e:\n    print(\"refused:\", e)\nprint(\"report.xlsx still has the good run:\", load_workbook(\"report.xlsx\").active[\"A2\"].value)\n",[14,1108,1109,1133,1151,1171,1181,1186,1199,1235,1239,1245,1268,1276,1286,1297],{"__ignoreMap":39},[43,1110,1111,1113,1116,1119,1121,1123,1126,1128,1131],{"class":45,"line":46},[43,1112,133],{"class":101},[43,1114,1115],{"class":136}," has_rows",[43,1117,1118],{"class":105},"(path, sheet",[43,1120,143],{"class":101},[43,1122,781],{"class":146},[43,1124,1125],{"class":105},", min_rows",[43,1127,143],{"class":101},[43,1129,1130],{"class":146},"2",[43,1132,150],{"class":105},[43,1134,1135,1137,1139,1142,1145,1147,1149],{"class":45,"line":109},[43,1136,156],{"class":105},[43,1138,143],{"class":101},[43,1140,1141],{"class":105}," load_workbook(path, ",[43,1143,1144],{"class":811},"read_only",[43,1146,143],{"class":101},[43,1148,984],{"class":146},[43,1150,312],{"class":105},[43,1152,1153,1155,1157,1160,1163,1166,1169],{"class":45,"line":123},[43,1154,167],{"class":105},[43,1156,143],{"class":101},[43,1158,1159],{"class":105}," wb[sheet] ",[43,1161,1162],{"class":101},"if",[43,1164,1165],{"class":105}," sheet ",[43,1167,1168],{"class":101},"else",[43,1170,172],{"class":105},[43,1172,1173,1176,1178],{"class":45,"line":130},[43,1174,1175],{"class":105},"    n ",[43,1177,143],{"class":101},[43,1179,1180],{"class":105}," ws.max_row\n",[43,1182,1183],{"class":45,"line":153},[43,1184,1185],{"class":105},"    wb.close()\n",[43,1187,1188,1190,1193,1196],{"class":45,"line":164},[43,1189,394],{"class":101},[43,1191,1192],{"class":105}," n ",[43,1194,1195],{"class":101},"\u003C",[43,1197,1198],{"class":105}," min_rows:\n",[43,1200,1201,1203,1206,1208,1210,1213,1215,1218,1220,1223,1225,1228,1230,1233],{"class":45,"line":175},[43,1202,416],{"class":101},[43,1204,1205],{"class":146}," ValueError",[43,1207,279],{"class":105},[43,1209,817],{"class":101},[43,1211,1212],{"class":53},"\"",[43,1214,824],{"class":823},[43,1216,1217],{"class":105},"path.name",[43,1219,830],{"class":823},[43,1221,1222],{"class":53}," has only ",[43,1224,824],{"class":823},[43,1226,1227],{"class":105},"n",[43,1229,830],{"class":823},[43,1231,1232],{"class":53}," row(s)\"",[43,1234,312],{"class":105},[43,1236,1237],{"class":45,"line":197},[43,1238,127],{"emptyLinePlaceholder":126},[43,1240,1241,1243],{"class":45,"line":215},[43,1242,463],{"class":101},[43,1244,410],{"class":105},[43,1246,1247,1250,1252,1254,1256,1259,1261,1264,1266],{"class":45,"line":229},[43,1248,1249],{"class":101},"    with",[43,1251,1003],{"class":105},[43,1253,255],{"class":53},[43,1255,67],{"class":105},[43,1257,1258],{"class":811},"validate",[43,1260,143],{"class":101},[43,1262,1263],{"class":105},"has_rows) ",[43,1265,487],{"class":101},[43,1267,1012],{"class":105},[43,1269,1270,1273],{"class":45,"line":238},[43,1271,1272],{"class":105},"        Workbook().save(tmp)                          ",[43,1274,1275],{"class":261},"# an empty report, e.g. the query returned nothing\n",[43,1277,1278,1280,1282,1284],{"class":45,"line":243},[43,1279,481],{"class":101},[43,1281,1205],{"class":146},[43,1283,568],{"class":101},[43,1285,490],{"class":105},[43,1287,1288,1290,1292,1295],{"class":45,"line":265},[43,1289,496],{"class":146},[43,1291,279],{"class":105},[43,1293,1294],{"class":53},"\"refused:\"",[43,1296,504],{"class":105},[43,1298,1299,1301,1303,1306,1308,1310,1312,1314],{"class":45,"line":270},[43,1300,1027],{"class":146},[43,1302,279],{"class":105},[43,1304,1305],{"class":53},"\"report.xlsx still has the good run:\"",[43,1307,1035],{"class":105},[43,1309,255],{"class":53},[43,1311,1040],{"class":105},[43,1313,1043],{"class":53},[43,1315,1046],{"class":105},[10,1317,1318,1319,1323],{},"The empty workbook is rejected, its temporary file is deleted, and the previous report stays in place. Put whatever checks your report needs here — expected sheets, a total that is not zero, a date in the title matching today; ",[23,1320,1322],{"href":1321},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel Report Before Sending It"," has a fuller set. Keeping the last good report is usually better than replacing it with a wrong one, as long as the failure is logged and alerts someone.",[29,1325,1327],{"id":1326},"step-4-use-it-with-pandas-and-xlsxwriter","Step 4 — Use it with pandas and XlsxWriter",[10,1329,1330],{},"The context manager yields a path, so it works with anything that writes to a path:",[34,1332,1334],{"className":92,"code":1333,"language":94,"meta":39,"style":39},"import pandas as pd\n\ndf = pd.DataFrame({\"Region\": [\"North\", \"South\"], \"Revenue\": [18400, 12100]})\nwith atomic_path(\"summary.xlsx\", validate=has_rows) as tmp:\n    with pd.ExcelWriter(tmp, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, sheet_name=\"Summary\", index=False)\nprint(pd.read_excel(\"summary.xlsx\").to_dict(\"records\"))\n",[14,1335,1336,1348,1352,1395,1416,1438,1463],{"__ignoreMap":39},[43,1337,1338,1340,1343,1345],{"class":45,"line":46},[43,1339,102],{"class":101},[43,1341,1342],{"class":105}," pandas ",[43,1344,487],{"class":101},[43,1346,1347],{"class":105}," pd\n",[43,1349,1350],{"class":45,"line":109},[43,1351,127],{"emptyLinePlaceholder":126},[43,1353,1354,1357,1359,1362,1365,1368,1371,1373,1376,1379,1382,1384,1387,1389,1392],{"class":45,"line":123},[43,1355,1356],{"class":105},"df ",[43,1358,143],{"class":101},[43,1360,1361],{"class":105}," pd.DataFrame({",[43,1363,1364],{"class":53},"\"Region\"",[43,1366,1367],{"class":105},": [",[43,1369,1370],{"class":53},"\"North\"",[43,1372,67],{"class":105},[43,1374,1375],{"class":53},"\"South\"",[43,1377,1378],{"class":105},"], ",[43,1380,1381],{"class":53},"\"Revenue\"",[43,1383,1367],{"class":105},[43,1385,1386],{"class":146},"18400",[43,1388,67],{"class":105},[43,1390,1391],{"class":146},"12100",[43,1393,1394],{"class":105},"]})\n",[43,1396,1397,1399,1401,1404,1406,1408,1410,1412,1414],{"class":45,"line":130},[43,1398,1000],{"class":101},[43,1400,1003],{"class":105},[43,1402,1403],{"class":53},"\"summary.xlsx\"",[43,1405,67],{"class":105},[43,1407,1258],{"class":811},[43,1409,143],{"class":101},[43,1411,1263],{"class":105},[43,1413,487],{"class":101},[43,1415,1012],{"class":105},[43,1417,1418,1420,1423,1426,1428,1431,1433,1435],{"class":45,"line":153},[43,1419,1249],{"class":101},[43,1421,1422],{"class":105}," pd.ExcelWriter(tmp, ",[43,1424,1425],{"class":811},"engine",[43,1427,143],{"class":101},[43,1429,1430],{"class":53},"\"xlsxwriter\"",[43,1432,898],{"class":105},[43,1434,487],{"class":101},[43,1436,1437],{"class":105}," writer:\n",[43,1439,1440,1443,1446,1448,1451,1453,1456,1458,1461],{"class":45,"line":164},[43,1441,1442],{"class":105},"        df.to_excel(writer, ",[43,1444,1445],{"class":811},"sheet_name",[43,1447,143],{"class":101},[43,1449,1450],{"class":53},"\"Summary\"",[43,1452,67],{"class":105},[43,1454,1455],{"class":811},"index",[43,1457,143],{"class":101},[43,1459,1460],{"class":146},"False",[43,1462,312],{"class":105},[43,1464,1465,1467,1470,1472,1475,1478],{"class":45,"line":175},[43,1466,1027],{"class":146},[43,1468,1469],{"class":105},"(pd.read_excel(",[43,1471,1403],{"class":53},[43,1473,1474],{"class":105},").to_dict(",[43,1476,1477],{"class":53},"\"records\"",[43,1479,1480],{"class":105},"))\n",[10,1482,1483,1484,1486,1487,1490],{},"The inner ",[14,1485,1000],{}," closes the ",[14,1488,1489],{},"ExcelWriter",", which is when XlsxWriter actually writes the file; only after that does the outer block validate and rename. Keep that nesting order — validating inside the writer block would check a file that has not been written yet. The same works for CSV exports, and for any other output a downstream process reads.",[598,1492,607,1497,607,1500,607,1503,607,1506,607,1513,607,1521,607,1526,607,1531,607,1535,607,1541,607,1546,607,1550,607,1557,607,1562,607,1565,607,1568,607,1571,607,1574,607,1579,607,1585,607,1590,607,1593,607,1596,607,1600,607,1603,607,1606,607,1612,607,1617],{"viewBox":1493,"role":601,"ariaLabelledBy":1494,"xmlns":605,"style":606},"4 0 752 142",[1495,1496],"at-steps-t","at-steps-d",[609,1498,1499],{"id":1495},"The atomic write, step by step",[613,1501,1502],{"id":1496},"Create a uniquely named temporary file in the target's folder, write the workbook to it, flush it to disk with fsync, validate it, copy the old file's permissions, then os.replace it over the target. Any failure deletes the temporary file and leaves the target unchanged.",[617,1504],{"x":619,"y":346,"width":620,"height":1505,"fill":622},"142",[617,1507],{"x":625,"y":626,"width":1508,"height":1509,"rx":1510,"fill":1511,"stroke":1512,"style":632},"153.0","110.5","12","#e7ebef","var(--line,#cdd5e6)",[634,1514,1516],{"transform":1515},"translate(84.5,29.0) scale(1.000)",[638,1517],{"d":1518,"fill":641,"stroke":1519,"style":1520},"M6 3h9l4 4v14H6z M15 3v4h4","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[645,1522,1095],{"x":1523,"y":1524,"style":1525},"96.5","75.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle",[645,1527,1530],{"x":1523,"y":1528,"style":1529},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","same folder, hidden",[645,1532,1534],{"x":1523,"y":1533,"style":1529},"112.5","name",[45,1536],{"x1":1537,"y1":1538,"x2":1539,"y2":1538,"stroke":1540,"style":632},"178.0","71.2","200.0","var(--brand,#5b5cf0)",[1542,1543],"polygon",{"points":1544,"fill":1545},"204.0,71.2 195.0,76.2 195.0,66.2","#5b5cf0",[617,1547],{"x":1548,"y":626,"width":1508,"height":1509,"rx":1510,"fill":1549,"stroke":1540,"style":632},"209.0","#f0f4ff",[634,1551,1553],{"transform":1552},"translate(273.5,29.0) scale(1.000)",[638,1554],{"d":1555,"fill":641,"stroke":1556,"style":1520},"M6 3h9l4 4v14H6z M15 3v4h4 M9.5 11.5l4.5 6 M14 11.5l-4.5 6","var(--brand-strong,#4338ca)",[645,1558,1561],{"x":1559,"y":1524,"style":1560},"285.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Write",[645,1563,1564],{"x":1559,"y":1528,"style":1529},"openpyxl, pandas,",[645,1566,1567],{"x":1559,"y":1533,"style":1529},"XlsxWriter",[45,1569],{"x1":1570,"y1":1538,"x2":672,"y2":1538,"stroke":1540,"style":632},"367.0",[1542,1572],{"points":1573,"fill":1545},"393.0,71.2 384.0,76.2 384.0,66.2",[617,1575],{"x":1576,"y":626,"width":1508,"height":1509,"rx":1510,"fill":1577,"stroke":1578,"style":632},"398.0","#fdefd8","var(--gold,#b4740a)",[634,1580,1582],{"transform":1581},"translate(462.5,29.0) scale(1.000)",[638,1583],{"d":680,"fill":641,"stroke":1584,"style":1520},"var(--gold-ink,#7a4e06)",[645,1586,1589],{"x":1587,"y":1524,"style":1588},"474.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","fsync + validate",[645,1591,1592],{"x":1587,"y":1528,"style":1529},"on disk, opens, has",[645,1594,1595],{"x":1587,"y":1533,"style":1529},"rows",[45,1597],{"x1":1598,"y1":1538,"x2":1599,"y2":1538,"stroke":1540,"style":632},"556.0","578.0",[1542,1601],{"points":1602,"fill":1545},"582.0,71.2 573.0,76.2 573.0,66.2",[617,1604],{"x":1605,"y":626,"width":1508,"height":1509,"rx":1510,"fill":673,"stroke":674,"style":632},"587.0",[634,1607,1609],{"transform":1608},"translate(651.5,37.8) scale(1.000)",[638,1610],{"d":1611,"fill":641,"stroke":681,"style":1520},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[645,1613,77],{"x":1614,"y":1615,"style":1616},"663.5","84.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[645,1618,1620],{"x":1614,"y":1619,"style":1529},"103.8","one atomic swap",[29,1622,1624],{"id":1623},"step-5-prove-readers-never-see-a-partial-file","Step 5 — Prove readers never see a partial file",[10,1626,1627],{},"A test can hammer the file with a reader while a writer replaces it repeatedly, and count how often the reader got something that is not a valid workbook:",[34,1629,1631],{"className":92,"code":1630,"language":94,"meta":39,"style":39},"import threading\n\nstop = threading.Event()\nseen = {\"ok\": 0, \"broken\": 0}\n\ndef reader():\n    while not stop.is_set():\n        try:\n            with zipfile.ZipFile(\"report.xlsx\") as z:\n                z.testzip()\n            seen[\"ok\"] += 1\n        except (zipfile.BadZipFile, FileNotFoundError, EOFError, OSError):\n            seen[\"broken\"] += 1\n\nt = threading.Thread(target=reader)\nt.start()\nfor i in range(15):\n    with atomic_path(\"report.xlsx\") as tmp:\n        make_workbook(f\"run {i}\", rows=3000).save(tmp)\nstop.set()\nt.join()\nprint(f\"reads: {seen['ok']} valid, {seen['broken']} broken\")\n",[14,1632,1633,1640,1644,1654,1681,1685,1695,1706,1713,1730,1735,1748,1771,1783,1787,1805,1810,1828,1842,1872,1877,1882],{"__ignoreMap":39},[43,1634,1635,1637],{"class":45,"line":46},[43,1636,102],{"class":101},[43,1638,1639],{"class":105}," threading\n",[43,1641,1642],{"class":45,"line":109},[43,1643,127],{"emptyLinePlaceholder":126},[43,1645,1646,1649,1651],{"class":45,"line":123},[43,1647,1648],{"class":105},"stop ",[43,1650,143],{"class":101},[43,1652,1653],{"class":105}," threading.Event()\n",[43,1655,1656,1659,1661,1663,1666,1668,1670,1672,1675,1677,1679],{"class":45,"line":130},[43,1657,1658],{"class":105},"seen ",[43,1660,143],{"class":101},[43,1662,337],{"class":105},[43,1664,1665],{"class":53},"\"ok\"",[43,1667,343],{"class":105},[43,1669,346],{"class":146},[43,1671,67],{"class":105},[43,1673,1674],{"class":53},"\"broken\"",[43,1676,343],{"class":105},[43,1678,346],{"class":146},[43,1680,349],{"class":105},[43,1682,1683],{"class":45,"line":153},[43,1684,127],{"emptyLinePlaceholder":126},[43,1686,1687,1689,1692],{"class":45,"line":164},[43,1688,133],{"class":101},[43,1690,1691],{"class":136}," reader",[43,1693,1694],{"class":105},"():\n",[43,1696,1697,1700,1703],{"class":45,"line":175},[43,1698,1699],{"class":101},"    while",[43,1701,1702],{"class":101}," not",[43,1704,1705],{"class":105}," stop.is_set():\n",[43,1707,1708,1711],{"class":45,"line":197},[43,1709,1710],{"class":101},"        try",[43,1712,410],{"class":105},[43,1714,1715,1718,1721,1723,1725,1727],{"class":45,"line":215},[43,1716,1717],{"class":101},"            with",[43,1719,1720],{"class":105}," zipfile.ZipFile(",[43,1722,255],{"class":53},[43,1724,898],{"class":105},[43,1726,487],{"class":101},[43,1728,1729],{"class":105}," z:\n",[43,1731,1732],{"class":45,"line":229},[43,1733,1734],{"class":105},"                z.testzip()\n",[43,1736,1737,1740,1742,1744,1746],{"class":45,"line":238},[43,1738,1739],{"class":105},"            seen[",[43,1741,1665],{"class":53},[43,1743,382],{"class":105},[43,1745,385],{"class":101},[43,1747,388],{"class":146},[43,1749,1750,1753,1756,1759,1761,1764,1766,1769],{"class":45,"line":243},[43,1751,1752],{"class":101},"        except",[43,1754,1755],{"class":105}," (zipfile.BadZipFile, ",[43,1757,1758],{"class":146},"FileNotFoundError",[43,1760,67],{"class":105},[43,1762,1763],{"class":146},"EOFError",[43,1765,67],{"class":105},[43,1767,1768],{"class":146},"OSError",[43,1770,150],{"class":105},[43,1772,1773,1775,1777,1779,1781],{"class":45,"line":265},[43,1774,1739],{"class":105},[43,1776,1674],{"class":53},[43,1778,382],{"class":105},[43,1780,385],{"class":101},[43,1782,388],{"class":146},[43,1784,1785],{"class":45,"line":270},[43,1786,127],{"emptyLinePlaceholder":126},[43,1788,1789,1792,1794,1797,1800,1802],{"class":45,"line":287},[43,1790,1791],{"class":105},"t ",[43,1793,143],{"class":101},[43,1795,1796],{"class":105}," threading.Thread(",[43,1798,1799],{"class":811},"target",[43,1801,143],{"class":101},[43,1803,1804],{"class":105},"reader)\n",[43,1806,1807],{"class":45,"line":293},[43,1808,1809],{"class":105},"t.start()\n",[43,1811,1812,1815,1817,1819,1821,1823,1826],{"class":45,"line":298},[43,1813,1814],{"class":101},"for",[43,1816,203],{"class":105},[43,1818,206],{"class":101},[43,1820,209],{"class":146},[43,1822,279],{"class":105},[43,1824,1825],{"class":146},"15",[43,1827,150],{"class":105},[43,1829,1830,1832,1834,1836,1838,1840],{"class":45,"line":315},[43,1831,1249],{"class":101},[43,1833,1003],{"class":105},[43,1835,255],{"class":53},[43,1837,898],{"class":105},[43,1839,487],{"class":101},[43,1841,1012],{"class":105},[43,1843,1844,1847,1849,1852,1854,1857,1859,1861,1863,1865,1867,1870],{"class":45,"line":329},[43,1845,1846],{"class":105},"        make_workbook(",[43,1848,817],{"class":101},[43,1850,1851],{"class":53},"\"run ",[43,1853,824],{"class":823},[43,1855,1856],{"class":105},"i",[43,1858,830],{"class":823},[43,1860,1212],{"class":53},[43,1862,67],{"class":105},[43,1864,1595],{"class":811},[43,1866,143],{"class":101},[43,1868,1869],{"class":146},"3000",[43,1871,1022],{"class":105},[43,1873,1874],{"class":45,"line":352},[43,1875,1876],{"class":105},"stop.set()\n",[43,1878,1879],{"class":45,"line":374},[43,1880,1881],{"class":105},"t.join()\n",[43,1883,1884,1886,1888,1890,1893,1895,1898,1901,1904,1906,1909,1911,1913,1916,1918,1920,1923],{"class":45,"line":391},[43,1885,1027],{"class":146},[43,1887,279],{"class":105},[43,1889,817],{"class":101},[43,1891,1892],{"class":53},"\"reads: ",[43,1894,824],{"class":823},[43,1896,1897],{"class":105},"seen[",[43,1899,1900],{"class":53},"'ok'",[43,1902,1903],{"class":105},"]",[43,1905,830],{"class":823},[43,1907,1908],{"class":53}," valid, ",[43,1910,824],{"class":823},[43,1912,1897],{"class":105},[43,1914,1915],{"class":53},"'broken'",[43,1917,1903],{"class":105},[43,1919,830],{"class":823},[43,1921,1922],{"class":53}," broken\"",[43,1924,312],{"class":105},[10,1926,1927,1928,1931],{},"Every read the reader made — and it makes many while the writer saves fifteen reports — found a complete, valid zip archive. Swap the context manager for a plain ",[14,1929,1930],{},"save(\"report.xlsx\")"," in the loop and the picture reverses: in the same test, the reader got 9 valid reads and more than 3,000 broken ones, because for most of each save the file on disk is incomplete. A test like this is worth keeping for any job whose output is polled by something else.",[29,1933,1935],{"id":1934},"windows-network-shares-and-sync-folders","Windows, network shares and sync folders",[1937,1938,1939,1961,1967,1973],"ul",{},[1940,1941,1942,1946,1947,1949,1950,1952,1953,1956,1957,27],"li",{},[1943,1944,1945],"strong",{},"Windows and open files."," If someone has ",[14,1948,16],{}," open in Excel, Excel holds a lock and ",[14,1951,77],{}," fails with ",[14,1954,1955],{},"PermissionError"," — but the old file is untouched and the temporary file is cleaned up. Retry after a pause, or write to a dated name and update a pointer; see ",[23,1958,1960],{"href":1959},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002F","Handle \"Permission Denied\" When Writing Excel in Python",[1940,1962,1963,1966],{},[1943,1964,1965],{},"Network shares (SMB, NFS)."," A rename within one share is atomic on the server for most configurations, which is what readers on other machines see. Always create the temporary file on the share itself, never locally then copy.",[1940,1968,1969,1972],{},[1943,1970,1971],{},"OneDrive, Dropbox and similar folders."," The sync client may upload the temporary file before the rename. The hidden, dotted name keeps most clients from syncing it, but where that matters, write outside the synced folder on the same drive and replace into it.",[1940,1974,1975,1978],{},[1943,1976,1977],{},"Many readers on a schedule."," If consumers cache a file handle, an atomic swap still works: they keep reading the old file until they reopen, then see the new one.",[598,1980,607,1985,607,1988,607,1991,607,1994,607,1998,607,2003,607,2009,607,2012,607,2016,607,2020,607,2025,607,2027,607,2031,607,2033,607,2036,607,2039,607,2043,607,2045,607,2048,607,2050,607,2053,607,2056,607,2060,607,2062,607,2066,607,2068,607,2071,607,2074,607,2078,607,2080,607,2084,607,2086],{"viewBox":1981,"role":601,"ariaLabelledBy":1982,"xmlns":605,"style":606},"4 0 752 208",[1983,1984],"at-where-t","at-where-d",[609,1986,1987],{"id":1983},"Where the atomic swap holds",[613,1989,1990],{"id":1984},"On the same local volume, os.replace is atomic on Linux, macOS and Windows. On Windows it fails safely with PermissionError if the file is open in Excel. On a network share it is atomic when the temporary file is created on the share. Across volumes it is not atomic.",[617,1992],{"x":619,"y":346,"width":620,"height":1993,"fill":622},"208",[617,1995],{"x":625,"y":626,"width":1996,"height":1997,"rx":346,"fill":622,"stroke":1512,"style":656},"154.7","40.0",[617,1999],{"x":2000,"y":626,"width":2001,"height":1997,"rx":346,"fill":1549,"stroke":1540,"style":2002},"174.7","282.6","stroke-width:1.2px",[645,2004,2008],{"x":2005,"y":2006,"style":2007},"316.1","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","atomic?",[617,2010],{"x":2011,"y":626,"width":2001,"height":1997,"rx":346,"fill":1549,"stroke":1540,"style":2002},"457.4",[645,2013,2015],{"x":2014,"y":2006,"style":2007},"598.7","note",[617,2017],{"x":625,"y":2018,"width":1996,"height":2019,"rx":346,"fill":1511,"stroke":1512,"style":656},"56.0","34.0",[645,2021,2024],{"x":2022,"y":648,"style":2023},"32.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","same local volume",[617,2026],{"x":2000,"y":2018,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2028,2030],{"x":2005,"y":2029,"style":1529},"77.0","✓",[617,2032],{"x":2011,"y":2018,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2034,2035],{"x":2014,"y":2029,"style":1529},"Linux, macOS, Windows",[617,2037],{"x":625,"y":2038,"width":1996,"height":2019,"rx":346,"fill":1511,"stroke":1512,"style":656},"90.0",[645,2040,2042],{"x":2022,"y":2041,"style":2023},"112.0","target open in Excel",[617,2044],{"x":2000,"y":2038,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2046,2047],{"x":2005,"y":659,"style":1529},"fails safely",[617,2049],{"x":2011,"y":2038,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2051,2052],{"x":2014,"y":659,"style":1529},"PermissionError, retry",[617,2054],{"x":625,"y":2055,"width":1996,"height":2019,"rx":346,"fill":1511,"stroke":1512,"style":656},"124.0",[645,2057,2059],{"x":2022,"y":2058,"style":2023},"146.0","network share",[617,2061],{"x":2000,"y":2055,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2063,2065],{"x":2005,"y":2064,"style":1529},"145.0","✓ usually",[617,2067],{"x":2011,"y":2055,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2069,2070],{"x":2014,"y":2064,"style":1529},"temp file on the share",[617,2072],{"x":625,"y":2073,"width":1996,"height":2019,"rx":346,"fill":1511,"stroke":1512,"style":656},"158.0",[645,2075,2077],{"x":2022,"y":2076,"style":2023},"180.0","temp dir elsewhere",[617,2079],{"x":2000,"y":2073,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2081,2083],{"x":2005,"y":2082,"style":1529},"179.0","✗",[617,2085],{"x":2011,"y":2073,"width":2001,"height":2019,"rx":346,"fill":622,"stroke":1512,"style":656},[645,2087,2088],{"x":2014,"y":2082,"style":1529},"copy, not rename",[29,2090,2092],{"id":2091},"common-pitfalls","Common pitfalls",[1937,2094,2095,2108,2114,2123,2129],{},[1940,2096,2097,2107],{},[1943,2098,2099,2100,2103,2104,27],{},"Temp file in ",[14,2101,2102],{},"\u002Ftmp"," or ",[14,2105,2106],{},"%TEMP%"," Different file system, so the rename is not atomic or fails.",[1940,2109,2110,2113],{},[1943,2111,2112],{},"Validating inside the writer block."," The file is written when the writer closes; validate after.",[1940,2115,2116,2119,2120,2122],{},[1943,2117,2118],{},"Losing permissions."," ",[14,2121,1095],{}," files are private; copy the target's mode.",[1940,2124,2125,2128],{},[1943,2126,2127],{},"Leaving temp files after errors."," Delete them in the exception path.",[1940,2130,2131,2134],{},[1943,2132,2133],{},"Retrying forever on Windows locks."," Retry a few times, then fail and alert.",[29,2136,2138],{"id":2137},"conclusion","Conclusion",[10,2140,2141,2142,2144,2145,2147],{},"Saving directly over a workbook exposes readers to a truncated file during every save and destroys the previous version when a save fails. Write to a uniquely named temporary file in the same folder, ",[14,2143,1091],{}," it, validate it, copy the old file's permissions, and ",[14,2146,77],{}," it over the target — wrapped in a context manager that deletes the temporary file on any error. Readers then always see either the complete old report or the complete new one, and a broken run leaves the last good report in place.",[29,2149,2151],{"id":2150},"frequently-asked-questions","Frequently asked questions",[10,2153,2154,2157],{},[1943,2155,2156],{},"How do I save an Excel file atomically in Python?","\nCreate a temporary file in the same folder with tempfile.mkstemp, save the workbook to it, fsync it, then call os.replace to rename it over the target. If anything fails, delete the temporary file.",[10,2159,2160,2163],{},[1943,2161,2162],{},"Why must the temporary file be in the same folder?","\nos.replace is only atomic within one file system. A temporary file elsewhere may be on another volume, where the rename fails or becomes a copy that readers can catch half-done.",[10,2165,2166,2169],{},[1943,2167,2168],{},"Does os.replace work on Windows?","\nYes, it replaces an existing file atomically on the same volume. If the target is open in Excel the call fails with PermissionError and leaves the old file intact.",[10,2171,2172,2175],{},[1943,2173,2174],{},"Can I check the workbook before it replaces the old one?","\nYes. Open the temporary file and check sheets, row counts or totals before os.replace. If a check fails, delete the temporary file and keep the previous report.",[29,2177,2179],{"id":2178},"related","Related",[1937,2181,2182,2188,2195,2202],{},[1940,2183,2184,2185,2187],{},"Up one level: ",[23,2186,26],{"href":25}," — making report jobs robust.",[1940,2189,2190,2194],{},[23,2191,2193],{"href":2192},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002F","Prevent Overlapping Runs of an Excel Report Job"," — one writer at a time.",[1940,2196,2197,2201],{},[23,2198,2200],{"href":2199},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fname-version-and-archive-generated-excel-reports\u002F","Name, Version and Archive Generated Excel Reports"," — keeping earlier versions too.",[1940,2203,2204,2208],{},[23,2205,2207],{"href":2206},"\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Fwrite-excel-files-to-a-network-share-from-python\u002F","Write Excel Files to a Network Share from Python"," — the share case in detail.",[2210,2211,2212],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":39,"searchDepth":109,"depth":109,"links":2214},[2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225],{"id":31,"depth":109,"text":32},{"id":81,"depth":109,"text":82},{"id":702,"depth":109,"text":703},{"id":1099,"depth":109,"text":1100},{"id":1326,"depth":109,"text":1327},{"id":1623,"depth":109,"text":1624},{"id":1934,"depth":109,"text":1935},{"id":2091,"depth":109,"text":2092},{"id":2137,"depth":109,"text":2138},{"id":2150,"depth":109,"text":2151},{"id":2178,"depth":109,"text":2179},"2026-10-02","Never leave a half-written workbook behind: why saving straight over report.xlsx can corrupt it when a script crashes or a reader opens it mid-write, the write-to-temp-then-os.replace pattern as a reusable context manager for openpyxl, pandas and XlsxWriter, validating before the swap, keeping permissions, and what changes on Windows, network shares and sync folders.","md",[2230,2232,2234,2236],{"q":2156,"a":2231},"Create a temporary file in the same folder with tempfile.mkstemp, save the workbook to it, fsync it, then call os.replace to rename it over the target. If anything fails, delete the temporary file.",{"q":2162,"a":2233},"os.replace is only atomic within one file system. A temporary file elsewhere may be on another volume, where the rename fails or becomes a copy that readers can catch half-done.",{"q":2168,"a":2235},"Yes, it replaces an existing file atomically on the same volume. If the target is open in Excel the call fails with PermissionError and leaves the old file intact.",{"q":2174,"a":2237},"Yes. Open the temporary file and check sheets, row counts or totals before os.replace. If a check fails, delete the temporary file and keep the previous report.","write excel files atomically in python",null,{"breadcrumb":2241},[2242,2245,2248],{"name":2243,"item":2244},"Home","\u002F",{"name":2246,"item":2247},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":26,"item":25},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python",{"title":2251,"description":2252},"Write Excel Files Atomically in Python with os.replace","Save Excel reports safely: write to a temp file in the same folder, fsync, validate, then os.replace over the target. A context manager for openpyxl and pandas, crash and concurrent-reader tests, Windows and share caveats.","write-excel-files-atomically-in-python","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fwrite-excel-files-atomically-in-python\u002Findex",[2256,2257],"reliable-jobs","writing-excel","how-to","xXRyp8NG_FzSmijwSt1Q1qIr5i4im2VfxUjB3-uigWI",[2261,2264],{"title":1322,"path":2262,"stem":2263,"children":-1},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002Findex",{"title":2265,"path":2266,"stem":2267,"children":-1},"Exporting Excel Reports to PDF","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Findex",[2269,2271],{"path":2270,"title":26},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation",{"path":2272,"title":2246},"\u002Fautomating-reporting-workflows",[],[2275,2279,2282,2285,2288,2291],{"path":2276,"title":2277,"kind":2278},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","Testing and Packaging Excel Automation Scripts","guide",{"path":2280,"title":2281,"kind":2258},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fblock-large-or-sensitive-excel-files-with-a-pre-commit-hook","Block Large or Sensitive Excel Files with a Pre-commit Hook",{"path":2283,"title":2284,"kind":2258},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fbuild-a-command-line-tool-for-excel-reports-with-argparse","Build a Command-Line Tool for Excel Reports with argparse",{"path":2286,"title":2287,"kind":2258},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests","Compare Excel Output to a Golden File in Tests",{"path":2289,"title":2290,"kind":2258},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory","Create Excel Test Fixtures in Memory for pytest",{"path":2292,"title":2293,"kind":2258},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver","Diff Excel Files in Git with a textconv Driver",1790972132295]