[{"data":1,"prerenderedAt":1928},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder":3,"surround:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder":1895,"ancestors:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder":1902,"children:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder":1907,"related:\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder":1908},{"id":4,"title":5,"body":6,"dateModified":1862,"datePublished":1862,"description":1863,"extension":1864,"faq":1865,"keyword":1874,"kind":1875,"meta":1876,"navigation":346,"path":1884,"seo":1885,"slug":1888,"stem":1889,"topics":1890,"type":1893,"__hash__":1894},"docs\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder\u002Findex.md","Ignore Excel Lock Files When Processing a Folder",{"type":7,"value":8,"toc":1849},"minimark",[9,48,53,81,111,294,298,304,506,533,537,540,823,839,843,854,1034,1045,1226,1230,1238,1347,1353,1357,1360,1613,1687,1699,1703,1716,1720,1764,1768,1784,1788,1794,1800,1806,1812,1816,1845],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,37,38,41,42,47],"p",{},"A script that loops over ",[14,15,16],"code",{},"glob(\"*.xlsx\")"," in a shared folder works in testing and fails on Monday morning with ",[14,19,20],{},"ValueError: Excel file format cannot be determined"," or ",[14,23,24],{},"BadZipFile: File is not a zip file",". The culprit is rarely a broken workbook. When someone has ",[14,27,28],{},"sales.xlsx"," open, Excel creates a small hidden file called ",[14,31,32],{},"~$sales.xlsx"," next to it — and that name matches ",[14,35,36],{},"*.xlsx",". LibreOffice creates ",[14,39,40],{},".~lock.sales.xlsx#",". A file still being copied into the folder is a truncated zip for a few seconds. None of these is a workbook to process, and a robust job recognises each and moves on, saying what it skipped. This page is part of ",[43,44,46],"a",{"href":45},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002F","Error Handling and Logging in Excel Automation",".",[49,50,52],"h2",{"id":51},"prerequisites","Prerequisites",[54,55,60],"pre",{"className":56,"code":57,"language":58,"meta":59,"style":59},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,61,62],{"__ignoreMap":59},[63,64,67,71,75,78],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sMTad","pip",[63,72,74],{"class":73},"srMev"," install",[63,76,77],{"class":73}," pandas",[63,79,80],{"class":73}," openpyxl\n",[10,82,83,86,87,90,91,94,95,98,99,102,103,106,107,110],{},[14,84,85],{},"inbox\u002F"," contains three good workbooks — ",[14,88,89],{},"north.xlsx",", ",[14,92,93],{},"south.xlsx"," and ",[14,96,97],{},"west.xlsx"," — and the debris of a real shared folder: an Excel owner file ",[14,100,101],{},"~$south.xlsx"," because someone has South open, a LibreOffice lock file for West, a temporary ",[14,104,105],{},"~WRL0913.tmp"," left by an interrupted save, ",[14,108,109],{},"east.xlsx"," still being copied (only half of it has arrived), and a text file.",[112,113,121,122,121,126,121,130,121,138,121,147,121,154,121,161,121,166,121,170,121,174,121,177,121,181,121,183,121,186,121,188,121,191,121,194,121,198,121,200,121,202,121,204,121,207,121,210,121,214,121,218,121,221,121,223,121,226,121,229,121,233,121,235,121,237,121,239,121,242,121,245,121,249,121,251,121,254,121,256,121,259,121,262,121,266,121,268,121,270,121,272,121,275,121,278,121,282,121,286,121,289,121,291],"svg",{"viewBox":114,"role":115,"ariaLabelledBy":116,"xmlns":119,"style":120},"170 0 420 256","img",[117,118],"lk-folder-t","lk-folder-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:420px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[123,124,125],"title",{"id":117},"What the folder really contains",[127,128,129],"desc",{"id":118},"A listing of the inbox folder. Three workbooks are real; the Excel owner file, the LibreOffice lock file, a temporary save file, a half-copied workbook and a text file are not workbooks to process.",[131,132],"rect",{"x":133,"y":134,"width":135,"height":136,"fill":137},"170","0","420","256","#ffffff",[131,139],{"x":140,"y":141,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"186.1","16.0","32.0","28.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[148,149,153],"text",{"x":150,"y":151,"style":152},"202.1","35.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","1",[131,155],{"x":156,"y":141,"width":157,"height":143,"rx":134,"fill":158,"stroke":159,"style":160},"218.1","134.2","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[148,162,165],{"x":163,"y":151,"style":164},"285.2","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","File",[131,167],{"x":168,"y":141,"width":169,"height":143,"rx":134,"fill":158,"stroke":159,"style":160},"352.3","221.6",[148,171,173],{"x":172,"y":151,"style":164},"463.1","What it is",[131,175],{"x":140,"y":176,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"44.0",[148,178,180],{"x":150,"y":179,"style":152},"63.0","2",[131,182],{"x":156,"y":176,"width":157,"height":143,"rx":134,"fill":137,"stroke":145,"style":146},[148,184,89],{"x":163,"y":179,"style":185},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[131,187],{"x":168,"y":176,"width":169,"height":143,"rx":134,"fill":137,"stroke":145,"style":146},[148,189,190],{"x":172,"y":179,"style":185},"workbook",[131,192],{"x":140,"y":193,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"72.0",[148,195,197],{"x":150,"y":196,"style":152},"91.0","3",[131,199],{"x":156,"y":193,"width":157,"height":143,"rx":134,"fill":137,"stroke":145,"style":146},[148,201,93],{"x":163,"y":196,"style":185},[131,203],{"x":168,"y":193,"width":169,"height":143,"rx":134,"fill":137,"stroke":145,"style":146},[148,205,206],{"x":172,"y":196,"style":185},"workbook — open in Excel",[131,208],{"x":140,"y":209,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"100.0",[148,211,213],{"x":150,"y":212,"style":152},"119.0","4",[131,215],{"x":156,"y":209,"width":157,"height":143,"rx":134,"fill":216,"stroke":217,"style":160},"#fdefd8","var(--gold,#b4740a)",[148,219,101],{"x":163,"y":212,"style":220},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[131,222],{"x":168,"y":209,"width":169,"height":143,"rx":134,"fill":216,"stroke":217,"style":160},[148,224,225],{"x":172,"y":212,"style":220},"Excel owner file",[131,227],{"x":140,"y":228,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"128.0",[148,230,232],{"x":150,"y":231,"style":152},"147.0","5",[131,234],{"x":156,"y":228,"width":157,"height":143,"rx":134,"fill":137,"stroke":145,"style":146},[148,236,97],{"x":163,"y":231,"style":185},[131,238],{"x":168,"y":228,"width":169,"height":143,"rx":134,"fill":137,"stroke":145,"style":146},[148,240,241],{"x":172,"y":231,"style":185},"workbook — open in LibreOffice",[131,243],{"x":140,"y":244,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"156.0",[148,246,248],{"x":150,"y":247,"style":152},"175.0","6",[131,250],{"x":156,"y":244,"width":157,"height":143,"rx":134,"fill":216,"stroke":217,"style":160},[148,252,253],{"x":163,"y":247,"style":220},".~lock.west.xlsx#",[131,255],{"x":168,"y":244,"width":169,"height":143,"rx":134,"fill":216,"stroke":217,"style":160},[148,257,258],{"x":172,"y":247,"style":220},"LibreOffice lock file",[131,260],{"x":140,"y":261,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"184.0",[148,263,265],{"x":150,"y":264,"style":152},"203.0","7",[131,267],{"x":156,"y":261,"width":157,"height":143,"rx":134,"fill":216,"stroke":217,"style":160},[148,269,105],{"x":163,"y":264,"style":220},[131,271],{"x":168,"y":261,"width":169,"height":143,"rx":134,"fill":216,"stroke":217,"style":160},[148,273,274],{"x":172,"y":264,"style":220},"temporary save file",[131,276],{"x":140,"y":277,"width":142,"height":143,"rx":134,"fill":144,"stroke":145,"style":146},"212.0",[148,279,281],{"x":150,"y":280,"style":152},"231.0","8",[131,283],{"x":156,"y":277,"width":157,"height":143,"rx":134,"fill":284,"stroke":285,"style":160},"#fce9e9","var(--danger,#dc2626)",[148,287,109],{"x":163,"y":280,"style":288},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[131,290],{"x":168,"y":277,"width":169,"height":143,"rx":134,"fill":284,"stroke":285,"style":160},[148,292,293],{"x":172,"y":280,"style":288},"still being copied",[49,295,297],{"id":296},"step-1-see-the-naive-loop-fail","Step 1 — See the naive loop fail",[10,299,300,303],{},[14,301,302],{},"Path.glob(\"*.xlsx\")"," matches Excel's owner file, and pandas cannot read it; the half-copied file is not a valid zip:",[54,305,309],{"className":306,"code":307,"language":308,"meta":59,"style":59},"language-python shiki shiki-themes github-light github-dark-high-contrast","from pathlib import Path\nimport pandas as pd\n\ninbox = Path(\"inbox\")\nprint(sorted(p.name for p in inbox.glob(\"*.xlsx\")))\nfor path in sorted(inbox.glob(\"*.xlsx\")):\n    try:\n        pd.read_excel(path)\n        print(\"read   \", path.name)\n    except Exception as err:\n        print(\"FAILED \", path.name, \"-\", type(err).__name__, str(err)[:48])\n","python",[14,310,311,327,341,348,366,400,421,430,436,450,465],{"__ignoreMap":59},[63,312,313,317,321,324],{"class":65,"line":66},[63,314,316],{"class":315},"s-kum","from",[63,318,320],{"class":319},"skGVy"," pathlib ",[63,322,323],{"class":315},"import",[63,325,326],{"class":319}," Path\n",[63,328,330,332,335,338],{"class":65,"line":329},2,[63,331,323],{"class":315},[63,333,334],{"class":319}," pandas ",[63,336,337],{"class":315},"as",[63,339,340],{"class":319}," pd\n",[63,342,344],{"class":65,"line":343},3,[63,345,347],{"emptyLinePlaceholder":346},true,"\n",[63,349,351,354,357,360,363],{"class":65,"line":350},4,[63,352,353],{"class":319},"inbox ",[63,355,356],{"class":315},"=",[63,358,359],{"class":319}," Path(",[63,361,362],{"class":73},"\"inbox\"",[63,364,365],{"class":319},")\n",[63,367,369,373,376,379,382,385,388,391,394,397],{"class":65,"line":368},5,[63,370,372],{"class":371},"sP0c6","print",[63,374,375],{"class":319},"(",[63,377,378],{"class":371},"sorted",[63,380,381],{"class":319},"(p.name ",[63,383,384],{"class":315},"for",[63,386,387],{"class":319}," p ",[63,389,390],{"class":315},"in",[63,392,393],{"class":319}," inbox.glob(",[63,395,396],{"class":73},"\"*.xlsx\"",[63,398,399],{"class":319},")))\n",[63,401,403,405,408,410,413,416,418],{"class":65,"line":402},6,[63,404,384],{"class":315},[63,406,407],{"class":319}," path ",[63,409,390],{"class":315},[63,411,412],{"class":371}," sorted",[63,414,415],{"class":319},"(inbox.glob(",[63,417,396],{"class":73},[63,419,420],{"class":319},")):\n",[63,422,424,427],{"class":65,"line":423},7,[63,425,426],{"class":315},"    try",[63,428,429],{"class":319},":\n",[63,431,433],{"class":65,"line":432},8,[63,434,435],{"class":319},"        pd.read_excel(path)\n",[63,437,439,442,444,447],{"class":65,"line":438},9,[63,440,441],{"class":371},"        print",[63,443,375],{"class":319},[63,445,446],{"class":73},"\"read   \"",[63,448,449],{"class":319},", path.name)\n",[63,451,453,456,459,462],{"class":65,"line":452},10,[63,454,455],{"class":315},"    except",[63,457,458],{"class":371}," Exception",[63,460,461],{"class":315}," as",[63,463,464],{"class":319}," err:\n",[63,466,468,470,472,475,478,481,483,486,489,492,494,497,500,503],{"class":65,"line":467},11,[63,469,441],{"class":371},[63,471,375],{"class":319},[63,473,474],{"class":73},"\"FAILED \"",[63,476,477],{"class":319},", path.name, ",[63,479,480],{"class":73},"\"-\"",[63,482,90],{"class":319},[63,484,485],{"class":371},"type",[63,487,488],{"class":319},"(err).",[63,490,491],{"class":371},"__name__",[63,493,90],{"class":319},[63,495,496],{"class":371},"str",[63,498,499],{"class":319},"(err)[:",[63,501,502],{"class":371},"48",[63,504,505],{"class":319},"])\n",[10,507,508,509,512,513,516,517,519,520,523,524,527,528,532],{},"Two of the five matches fail: the ",[14,510,511],{},"~$"," owner file with \"Excel file format cannot be determined\" — pandas looks at its first bytes and finds neither a zip nor an old ",[14,514,515],{},".xls"," — and ",[14,518,109],{}," with ",[14,521,522],{},"BadZipFile",", because the zip directory at the end of the file has not arrived yet. In a job without the ",[14,525,526],{},"try",", the first failure stops everything, and the reports after it in alphabetical order are never built. ",[43,529,531],{"href":530},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-badzipfile-error-when-reading-excel-in-python\u002F","Fix BadZipFile When Reading an Excel File in Python"," covers the error in general.",[49,534,536],{"id":535},"step-2-filter-out-lock-and-temporary-files-by-name","Step 2 — Filter out lock and temporary files by name",[10,538,539],{},"The debris has recognisable names. Filter it before anything tries to open a file:",[54,541,543],{"className":306,"code":542,"language":308,"meta":59,"style":59},"import re\n\nWORKBOOK_SUFFIXES = {\".xlsx\", \".xlsm\", \".xls\", \".xlsb\"}\nIGNORE = re.compile(r\"^(~\\$|\\.~lock\\.|~WRL|\\.)\", re.IGNORECASE)\n\ndef candidate_workbooks(folder):\n    for path in sorted(folder.iterdir()):\n        if not path.is_file():\n            continue\n        if IGNORE.match(path.name):\n            yield path, \"lock or temporary file\"\n        elif path.suffix.lower() not in WORKBOOK_SUFFIXES:\n            yield path, \"not a workbook\"\n        else:\n            yield path, None\n\nfor path, reason in candidate_workbooks(inbox):\n    print(f\"{path.name:22} {reason or 'candidate'}\")\n",[14,544,545,552,556,588,647,651,663,677,688,693,703,714,734,744,752,762,767,780],{"__ignoreMap":59},[63,546,547,549],{"class":65,"line":66},[63,548,323],{"class":315},[63,550,551],{"class":319}," re\n",[63,553,554],{"class":65,"line":329},[63,555,347],{"emptyLinePlaceholder":346},[63,557,558,561,564,567,570,572,575,577,580,582,585],{"class":65,"line":343},[63,559,560],{"class":371},"WORKBOOK_SUFFIXES",[63,562,563],{"class":315}," =",[63,565,566],{"class":319}," {",[63,568,569],{"class":73},"\".xlsx\"",[63,571,90],{"class":319},[63,573,574],{"class":73},"\".xlsm\"",[63,576,90],{"class":319},[63,578,579],{"class":73},"\".xls\"",[63,581,90],{"class":319},[63,583,584],{"class":73},"\".xlsb\"",[63,586,587],{"class":319},"}\n",[63,589,590,593,595,598,601,604,607,610,614,617,620,623,625,627,630,632,634,637,639,642,645],{"class":65,"line":350},[63,591,592],{"class":371},"IGNORE",[63,594,563],{"class":315},[63,596,597],{"class":319}," re.compile(",[63,599,600],{"class":315},"r",[63,602,603],{"class":73},"\"",[63,605,606],{"class":371},"^(",[63,608,609],{"class":73},"~",[63,611,613],{"class":612},"s_b0D","\\$",[63,615,616],{"class":315},"|",[63,618,619],{"class":612},"\\.",[63,621,622],{"class":73},"~lock",[63,624,619],{"class":612},[63,626,616],{"class":315},[63,628,629],{"class":73},"~WRL",[63,631,616],{"class":315},[63,633,619],{"class":612},[63,635,636],{"class":371},")",[63,638,603],{"class":73},[63,640,641],{"class":319},", re.",[63,643,644],{"class":371},"IGNORECASE",[63,646,365],{"class":319},[63,648,649],{"class":65,"line":368},[63,650,347],{"emptyLinePlaceholder":346},[63,652,653,656,660],{"class":65,"line":402},[63,654,655],{"class":315},"def",[63,657,659],{"class":658},"s_Opv"," candidate_workbooks",[63,661,662],{"class":319},"(folder):\n",[63,664,665,668,670,672,674],{"class":65,"line":423},[63,666,667],{"class":315},"    for",[63,669,407],{"class":319},[63,671,390],{"class":315},[63,673,412],{"class":371},[63,675,676],{"class":319},"(folder.iterdir()):\n",[63,678,679,682,685],{"class":65,"line":432},[63,680,681],{"class":315},"        if",[63,683,684],{"class":315}," not",[63,686,687],{"class":319}," path.is_file():\n",[63,689,690],{"class":65,"line":438},[63,691,692],{"class":315},"            continue\n",[63,694,695,697,700],{"class":65,"line":452},[63,696,681],{"class":315},[63,698,699],{"class":371}," IGNORE",[63,701,702],{"class":319},".match(path.name):\n",[63,704,705,708,711],{"class":65,"line":467},[63,706,707],{"class":315},"            yield",[63,709,710],{"class":319}," path, ",[63,712,713],{"class":73},"\"lock or temporary file\"\n",[63,715,717,720,723,726,729,732],{"class":65,"line":716},12,[63,718,719],{"class":315},"        elif",[63,721,722],{"class":319}," path.suffix.lower() ",[63,724,725],{"class":315},"not",[63,727,728],{"class":315}," in",[63,730,731],{"class":371}," WORKBOOK_SUFFIXES",[63,733,429],{"class":319},[63,735,737,739,741],{"class":65,"line":736},13,[63,738,707],{"class":315},[63,740,710],{"class":319},[63,742,743],{"class":73},"\"not a workbook\"\n",[63,745,747,750],{"class":65,"line":746},14,[63,748,749],{"class":315},"        else",[63,751,429],{"class":319},[63,753,755,757,759],{"class":65,"line":754},15,[63,756,707],{"class":315},[63,758,710],{"class":319},[63,760,761],{"class":371},"None\n",[63,763,765],{"class":65,"line":764},16,[63,766,347],{"emptyLinePlaceholder":346},[63,768,770,772,775,777],{"class":65,"line":769},17,[63,771,384],{"class":315},[63,773,774],{"class":319}," path, reason ",[63,776,390],{"class":315},[63,778,779],{"class":319}," candidate_workbooks(inbox):\n",[63,781,783,786,788,791,793,797,800,803,806,808,811,814,817,819,821],{"class":65,"line":782},18,[63,784,785],{"class":371},"    print",[63,787,375],{"class":319},[63,789,790],{"class":315},"f",[63,792,603],{"class":73},[63,794,796],{"class":795},"sSjpA","{",[63,798,799],{"class":319},"path.name",[63,801,802],{"class":315},":22",[63,804,805],{"class":795},"}",[63,807,566],{"class":795},[63,809,810],{"class":319},"reason ",[63,812,813],{"class":315},"or",[63,815,816],{"class":73}," 'candidate'",[63,818,805],{"class":795},[63,820,603],{"class":73},[63,822,365],{"class":319},[10,824,825,827,828,831,832,834,835,838],{},[14,826,511],{}," marks Excel's owner files, ",[14,829,830],{},".~lock."," LibreOffice's, ",[14,833,629],{}," Excel's temporary files during a save, and a leading dot hidden files in general — ",[14,836,837],{},".DS_Store"," from a Mac user, for example. Each skip carries a reason so the job's log can say what it ignored instead of silently processing fewer files than expected.",[49,840,842],{"id":841},"step-3-skip-files-that-are-not-complete-yet","Step 3 — Skip files that are not complete yet",[10,844,845,846,849,850,853],{},"A workbook being copied has the right name before it has the right content. An ",[14,847,848],{},".xlsx"," file is a zip archive, and ",[14,851,852],{},"zipfile.is_zipfile"," checks for the directory at the end that only exists once the whole file is there. Files that pass by name are checked for that, and — for jobs that watch a folder — for a size that has stopped changing:",[54,855,857],{"className":306,"code":856,"language":308,"meta":59,"style":59},"import time\nimport zipfile\n\ndef is_complete(path, settle_seconds=0.0):\n    if path.suffix.lower() == \".xls\":\n        return path.stat().st_size > 0            # old binary format, not a zip\n    if settle_seconds:\n        size = path.stat().st_size\n        time.sleep(settle_seconds)\n        if path.stat().st_size != size:\n            return False\n    return zipfile.is_zipfile(path)\n\nfor name in (\"north.xlsx\", \"east.xlsx\"):\n    print(name, \"complete\" if is_complete(inbox \u002F name) else \"incomplete — try again later\")\n",[14,858,859,866,873,877,895,910,928,935,945,950,962,970,978,982,1004],{"__ignoreMap":59},[63,860,861,863],{"class":65,"line":66},[63,862,323],{"class":315},[63,864,865],{"class":319}," time\n",[63,867,868,870],{"class":65,"line":329},[63,869,323],{"class":315},[63,871,872],{"class":319}," zipfile\n",[63,874,875],{"class":65,"line":343},[63,876,347],{"emptyLinePlaceholder":346},[63,878,879,881,884,887,889,892],{"class":65,"line":350},[63,880,655],{"class":315},[63,882,883],{"class":658}," is_complete",[63,885,886],{"class":319},"(path, settle_seconds",[63,888,356],{"class":315},[63,890,891],{"class":371},"0.0",[63,893,894],{"class":319},"):\n",[63,896,897,900,902,905,908],{"class":65,"line":368},[63,898,899],{"class":315},"    if",[63,901,722],{"class":319},[63,903,904],{"class":315},"==",[63,906,907],{"class":73}," \".xls\"",[63,909,429],{"class":319},[63,911,912,915,918,921,924],{"class":65,"line":402},[63,913,914],{"class":315},"        return",[63,916,917],{"class":319}," path.stat().st_size ",[63,919,920],{"class":315},">",[63,922,923],{"class":371}," 0",[63,925,927],{"class":926},"s-wDw","            # old binary format, not a zip\n",[63,929,930,932],{"class":65,"line":423},[63,931,899],{"class":315},[63,933,934],{"class":319}," settle_seconds:\n",[63,936,937,940,942],{"class":65,"line":432},[63,938,939],{"class":319},"        size ",[63,941,356],{"class":315},[63,943,944],{"class":319}," path.stat().st_size\n",[63,946,947],{"class":65,"line":438},[63,948,949],{"class":319},"        time.sleep(settle_seconds)\n",[63,951,952,954,956,959],{"class":65,"line":452},[63,953,681],{"class":315},[63,955,917],{"class":319},[63,957,958],{"class":315},"!=",[63,960,961],{"class":319}," size:\n",[63,963,964,967],{"class":65,"line":467},[63,965,966],{"class":315},"            return",[63,968,969],{"class":371}," False\n",[63,971,972,975],{"class":65,"line":716},[63,973,974],{"class":315},"    return",[63,976,977],{"class":319}," zipfile.is_zipfile(path)\n",[63,979,980],{"class":65,"line":736},[63,981,347],{"emptyLinePlaceholder":346},[63,983,984,986,989,991,994,997,999,1002],{"class":65,"line":746},[63,985,384],{"class":315},[63,987,988],{"class":319}," name ",[63,990,390],{"class":315},[63,992,993],{"class":319}," (",[63,995,996],{"class":73},"\"north.xlsx\"",[63,998,90],{"class":319},[63,1000,1001],{"class":73},"\"east.xlsx\"",[63,1003,894],{"class":319},[63,1005,1006,1008,1011,1014,1017,1020,1023,1026,1029,1032],{"class":65,"line":754},[63,1007,785],{"class":371},[63,1009,1010],{"class":319},"(name, ",[63,1012,1013],{"class":73},"\"complete\"",[63,1015,1016],{"class":315}," if",[63,1018,1019],{"class":319}," is_complete(inbox ",[63,1021,1022],{"class":315},"\u002F",[63,1024,1025],{"class":319}," name) ",[63,1027,1028],{"class":315},"else",[63,1030,1031],{"class":73}," \"incomplete — try again later\"",[63,1033,365],{"class":319},[10,1035,1036,1039,1040,1044],{},[14,1037,1038],{},"is_zipfile"," reads only the end of the file, so the check is cheap even for large workbooks. The size-settle check catches a copy that happens to pause at a moment when the partial file looks like a valid zip — rare, but possible with some copy tools. Incomplete files are left for the next run rather than treated as errors: they are not broken, just not ready. ",[43,1041,1043],{"href":1042},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fwatch-a-folder-and-process-new-excel-files\u002F","Watch a Folder and Process New Excel Files"," uses the same idea for event-driven jobs.",[112,1046,121,1052,121,1055,121,1058,121,1062,121,1069,121,1080,121,1085,121,1089,121,1093,121,1096,121,1101,121,1109,121,1114,121,1120,121,1127,121,1132,121,1137,121,1140,121,1143,121,1147,121,1150,121,1153,121,1160,121,1164,121,1167,121,1170,121,1173,121,1177,121,1180,121,1183,121,1189,121,1192,121,1195,121,1197,121,1200,121,1204,121,1207,121,1212,121,1219,121,1223],{"viewBox":1047,"role":115,"ariaLabelledBy":1048,"xmlns":119,"style":1051},"4 0 752 272",[1049,1050],"lk-decide-t","lk-decide-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[123,1053,1054],{"id":1049},"What to do with each file in the folder",[127,1056,1057],{"id":1050},"A decision diagram. Files whose names mark them as lock or temporary files are skipped; files that are not workbooks are skipped; workbooks that are not complete zip files are left for the next run; complete workbooks that another program has open are processed with a warning; the rest are processed.",[131,1059],{"x":213,"y":134,"width":1060,"height":1061,"fill":137},"752","272",[131,1063],{"x":1064,"y":141,"width":1065,"height":1066,"rx":1067,"fill":158,"stroke":159,"style":1068},"240.0","280.0","75.5","12","stroke-width:2px",[1070,1071,1073],"g",{"transform":1072},"translate(368.0,29.0) scale(1.000)",[1074,1075],"path",{"d":1076,"fill":1077,"stroke":1078,"style":1079},"M6 3h9l4 4v14H6z M15 3v4h4","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[148,1081,1084],{"x":1082,"y":1066,"style":1083},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What is this file?",[65,1086],{"x1":1082,"y1":1087,"x2":1082,"y2":1088,"stroke":159,"style":1068},"91.5","111.5",[65,1090],{"x1":1091,"y1":1088,"x2":1092,"y2":1088,"stroke":159,"style":1068},"104.0","656.0",[65,1094],{"x1":1091,"y1":1088,"x2":1091,"y2":1095,"stroke":159,"style":1068},"157.5",[1097,1098],"polygon",{"points":1099,"fill":1100},"104.0,161.5 99.0,152.5 109.0,152.5","#5b5cf0",[131,1102],{"x":1103,"y":1104,"width":1105,"height":1106,"rx":1107,"fill":137,"stroke":145,"style":1108},"43.8","121.5","120.4","21.0","10","stroke-width:1.2px",[148,1110,1113],{"x":1091,"y":1111,"style":1112},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","~$, .~lock, ~WRL",[131,1115],{"x":1116,"y":1117,"width":1118,"height":1119,"rx":1067,"fill":144,"stroke":145,"style":1068},"20.0","163.5","168.0","93.0",[1070,1121,1123],{"transform":1122},"translate(92.0,176.5) scale(1.000)",[1074,1124],{"d":1125,"fill":1077,"stroke":1126,"style":1079},"M3 5h18l-7 8v6l-4 2v-8z","var(--text,#172033)",[148,1128,1131],{"x":1091,"y":1129,"style":1130},"223.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Skip",[148,1133,1136],{"x":1091,"y":1134,"style":1135},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","lock or temp file",[65,1138],{"x1":1139,"y1":1088,"x2":1139,"y2":1095,"stroke":159,"style":1068},"288.0",[1097,1141],{"points":1142,"fill":1100},"288.0,161.5 283.0,152.5 293.0,152.5",[131,1144],{"x":1145,"y":1104,"width":1146,"height":1106,"rx":1107,"fill":137,"stroke":145,"style":1108},"243.2","89.6",[148,1148,1149],{"x":1139,"y":1111,"style":1112},"not a zip yet",[131,1151],{"x":1152,"y":1117,"width":1118,"height":1119,"rx":1067,"fill":216,"stroke":217,"style":1068},"204.0",[1070,1154,1156],{"transform":1155},"translate(276.0,176.5) scale(1.000)",[1074,1157],{"d":1158,"fill":1077,"stroke":1159,"style":1079},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","var(--gold-ink,#7a4e06)",[148,1161,1163],{"x":1139,"y":1129,"style":1162},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Leave for later",[148,1165,1166],{"x":1139,"y":1134,"style":1135},"still copying",[65,1168],{"x1":1169,"y1":1088,"x2":1169,"y2":1095,"stroke":159,"style":1068},"472.0",[1097,1171],{"points":1172,"fill":1100},"472.0,161.5 467.0,152.5 477.0,152.5",[131,1174],{"x":1175,"y":1104,"width":1176,"height":1106,"rx":1107,"fill":137,"stroke":145,"style":1108},"415.9","112.1",[148,1178,1179],{"x":1169,"y":1111,"style":1112},"open elsewhere",[131,1181],{"x":1182,"y":1117,"width":1118,"height":1119,"rx":1067,"fill":158,"stroke":159,"style":1068},"388.0",[1070,1184,1186],{"transform":1185},"translate(460.0,176.5) scale(1.000)",[1074,1187],{"d":1188,"fill":1077,"stroke":1078,"style":1079},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4",[148,1190,1191],{"x":1169,"y":1129,"style":1083},"Process + warn",[148,1193,1194],{"x":1169,"y":1134,"style":1135},"last saved version",[65,1196],{"x1":1092,"y1":1088,"x2":1092,"y2":1095,"stroke":159,"style":1068},[1097,1198],{"points":1199,"fill":1100},"656.0,161.5 651.0,152.5 661.0,152.5",[131,1201],{"x":1202,"y":1104,"width":1203,"height":1106,"rx":1107,"fill":137,"stroke":145,"style":1108},"619.4","73.2",[148,1205,1206],{"x":1092,"y":1111,"style":1112},"complete",[131,1208],{"x":1209,"y":1117,"width":1118,"height":1119,"rx":1067,"fill":1210,"stroke":1211,"style":1068},"572.0","#d9f4f1","var(--teal,#0f9488)",[1070,1213,1215],{"transform":1214},"translate(644.0,176.5) scale(1.000)",[1074,1216],{"d":1217,"fill":1077,"stroke":1218,"style":1079},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[148,1220,1222],{"x":1092,"y":1129,"style":1221},"font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Process",[148,1224,1225],{"x":1092,"y":1134,"style":1135},"read normally",[49,1227,1229],{"id":1228},"step-4-notice-workbooks-that-are-open-elsewhere","Step 4 — Notice workbooks that are open elsewhere",[10,1231,1232,1233,1237],{},"An owner or lock file next to a workbook means someone has it open. The file on disk is still readable — it is the last ",[1234,1235,1236],"em",{},"saved"," version — but it may not be what that person is looking at, and it may change minutes later. Whether to process it, warn or skip it is a policy decision; the job should at least know:",[54,1239,1241],{"className":306,"code":1240,"language":308,"meta":59,"style":59},"def open_elsewhere(path):\n    return (path.with_name(\"~$\" + path.name).exists()\n            or path.with_name(f\".~lock.{path.name}#\").exists())\n\nfor name in (\"north.xlsx\", \"south.xlsx\", \"west.xlsx\"):\n    print(name, \"open elsewhere\" if open_elsewhere(inbox \u002F name) else \"not open\")\n",[14,1242,1243,1253,1269,1294,1298,1322],{"__ignoreMap":59},[63,1244,1245,1247,1250],{"class":65,"line":66},[63,1246,655],{"class":315},[63,1248,1249],{"class":658}," open_elsewhere",[63,1251,1252],{"class":319},"(path):\n",[63,1254,1255,1257,1260,1263,1266],{"class":65,"line":329},[63,1256,974],{"class":315},[63,1258,1259],{"class":319}," (path.with_name(",[63,1261,1262],{"class":73},"\"~$\"",[63,1264,1265],{"class":315}," +",[63,1267,1268],{"class":319}," path.name).exists()\n",[63,1270,1271,1274,1277,1279,1282,1284,1286,1288,1291],{"class":65,"line":343},[63,1272,1273],{"class":315},"            or",[63,1275,1276],{"class":319}," path.with_name(",[63,1278,790],{"class":315},[63,1280,1281],{"class":73},"\".~lock.",[63,1283,796],{"class":795},[63,1285,799],{"class":319},[63,1287,805],{"class":795},[63,1289,1290],{"class":73},"#\"",[63,1292,1293],{"class":319},").exists())\n",[63,1295,1296],{"class":65,"line":350},[63,1297,347],{"emptyLinePlaceholder":346},[63,1299,1300,1302,1304,1306,1308,1310,1312,1315,1317,1320],{"class":65,"line":368},[63,1301,384],{"class":315},[63,1303,988],{"class":319},[63,1305,390],{"class":315},[63,1307,993],{"class":319},[63,1309,996],{"class":73},[63,1311,90],{"class":319},[63,1313,1314],{"class":73},"\"south.xlsx\"",[63,1316,90],{"class":319},[63,1318,1319],{"class":73},"\"west.xlsx\"",[63,1321,894],{"class":319},[63,1323,1324,1326,1328,1331,1333,1336,1338,1340,1342,1345],{"class":65,"line":402},[63,1325,785],{"class":371},[63,1327,1010],{"class":319},[63,1329,1330],{"class":73},"\"open elsewhere\"",[63,1332,1016],{"class":315},[63,1334,1335],{"class":319}," open_elsewhere(inbox ",[63,1337,1022],{"class":315},[63,1339,1025],{"class":319},[63,1341,1028],{"class":315},[63,1343,1344],{"class":73}," \"not open\"",[63,1346,365],{"class":319},[10,1348,1349,1350,1352],{},"On a Windows file share the names are case-insensitive, so compare them in lower case if users' files come with mixed capitalisation. An owner file also survives a crash: if Excel was killed, ",[14,1351,32],{}," stays behind although nobody has the file open. That is another reason to warn rather than refuse, and to report stale owner files older than a day so someone can delete them.",[49,1354,1356],{"id":1355},"step-5-put-it-together-with-a-skip-report","Step 5 — Put it together with a skip report",[10,1358,1359],{},"The loop now processes what is ready, leaves what is not, and records every decision:",[54,1361,1363],{"className":306,"code":1362,"language":308,"meta":59,"style":59},"from dataclasses import dataclass\n\n@dataclass\nclass Outcome:\n    file: str\n    action: str\n    detail: str = \"\"\n\ndef process_folder(folder):\n    outcomes, frames = [], []\n    for path, reason in candidate_workbooks(folder):\n        if reason:\n            outcomes.append(Outcome(path.name, \"skipped\", reason))\n        elif not is_complete(path):\n            outcomes.append(Outcome(path.name, \"deferred\", \"not a complete workbook yet\"))\n        else:\n            frames.append(pd.read_excel(path))\n            note = \"open in another program — last saved version used\" if open_elsewhere(path) else \"\"\n            outcomes.append(Outcome(path.name, \"processed\", note))\n    return pd.concat(frames, ignore_index=True), pd.DataFrame(outcomes)\n\ncombined, report = process_folder(inbox)\nprint(combined.to_string(index=False))\nprint(report.to_string(index=False))\n",[14,1364,1365,1377,1381,1386,1396,1408,1415,1427,1431,1440,1450,1461,1468,1479,1488,1503,1509,1514,1533,1544,1563,1568,1579,1597],{"__ignoreMap":59},[63,1366,1367,1369,1372,1374],{"class":65,"line":66},[63,1368,316],{"class":315},[63,1370,1371],{"class":319}," dataclasses ",[63,1373,323],{"class":315},[63,1375,1376],{"class":319}," dataclass\n",[63,1378,1379],{"class":65,"line":329},[63,1380,347],{"emptyLinePlaceholder":346},[63,1382,1383],{"class":65,"line":343},[63,1384,1385],{"class":658},"@dataclass\n",[63,1387,1388,1391,1394],{"class":65,"line":350},[63,1389,1390],{"class":315},"class",[63,1392,1393],{"class":69}," Outcome",[63,1395,429],{"class":319},[63,1397,1398,1402,1405],{"class":65,"line":368},[63,1399,1401],{"class":1400},"sa561","    file",[63,1403,1404],{"class":319},": ",[63,1406,1407],{"class":371},"str\n",[63,1409,1410,1413],{"class":65,"line":402},[63,1411,1412],{"class":319},"    action: ",[63,1414,1407],{"class":371},[63,1416,1417,1420,1422,1424],{"class":65,"line":423},[63,1418,1419],{"class":319},"    detail: ",[63,1421,496],{"class":371},[63,1423,563],{"class":315},[63,1425,1426],{"class":73}," \"\"\n",[63,1428,1429],{"class":65,"line":432},[63,1430,347],{"emptyLinePlaceholder":346},[63,1432,1433,1435,1438],{"class":65,"line":438},[63,1434,655],{"class":315},[63,1436,1437],{"class":658}," process_folder",[63,1439,662],{"class":319},[63,1441,1442,1445,1447],{"class":65,"line":452},[63,1443,1444],{"class":319},"    outcomes, frames ",[63,1446,356],{"class":315},[63,1448,1449],{"class":319}," [], []\n",[63,1451,1452,1454,1456,1458],{"class":65,"line":467},[63,1453,667],{"class":315},[63,1455,774],{"class":319},[63,1457,390],{"class":315},[63,1459,1460],{"class":319}," candidate_workbooks(folder):\n",[63,1462,1463,1465],{"class":65,"line":716},[63,1464,681],{"class":315},[63,1466,1467],{"class":319}," reason:\n",[63,1469,1470,1473,1476],{"class":65,"line":736},[63,1471,1472],{"class":319},"            outcomes.append(Outcome(path.name, ",[63,1474,1475],{"class":73},"\"skipped\"",[63,1477,1478],{"class":319},", reason))\n",[63,1480,1481,1483,1485],{"class":65,"line":746},[63,1482,719],{"class":315},[63,1484,684],{"class":315},[63,1486,1487],{"class":319}," is_complete(path):\n",[63,1489,1490,1492,1495,1497,1500],{"class":65,"line":754},[63,1491,1472],{"class":319},[63,1493,1494],{"class":73},"\"deferred\"",[63,1496,90],{"class":319},[63,1498,1499],{"class":73},"\"not a complete workbook yet\"",[63,1501,1502],{"class":319},"))\n",[63,1504,1505,1507],{"class":65,"line":764},[63,1506,749],{"class":315},[63,1508,429],{"class":319},[63,1510,1511],{"class":65,"line":769},[63,1512,1513],{"class":319},"            frames.append(pd.read_excel(path))\n",[63,1515,1516,1519,1521,1524,1526,1529,1531],{"class":65,"line":782},[63,1517,1518],{"class":319},"            note ",[63,1520,356],{"class":315},[63,1522,1523],{"class":73}," \"open in another program — last saved version used\"",[63,1525,1016],{"class":315},[63,1527,1528],{"class":319}," open_elsewhere(path) ",[63,1530,1028],{"class":315},[63,1532,1426],{"class":73},[63,1534,1536,1538,1541],{"class":65,"line":1535},19,[63,1537,1472],{"class":319},[63,1539,1540],{"class":73},"\"processed\"",[63,1542,1543],{"class":319},", note))\n",[63,1545,1547,1549,1552,1555,1557,1560],{"class":65,"line":1546},20,[63,1548,974],{"class":315},[63,1550,1551],{"class":319}," pd.concat(frames, ",[63,1553,1554],{"class":1400},"ignore_index",[63,1556,356],{"class":315},[63,1558,1559],{"class":371},"True",[63,1561,1562],{"class":319},"), pd.DataFrame(outcomes)\n",[63,1564,1566],{"class":65,"line":1565},21,[63,1567,347],{"emptyLinePlaceholder":346},[63,1569,1571,1574,1576],{"class":65,"line":1570},22,[63,1572,1573],{"class":319},"combined, report ",[63,1575,356],{"class":315},[63,1577,1578],{"class":319}," process_folder(inbox)\n",[63,1580,1582,1584,1587,1590,1592,1595],{"class":65,"line":1581},23,[63,1583,372],{"class":371},[63,1585,1586],{"class":319},"(combined.to_string(",[63,1588,1589],{"class":1400},"index",[63,1591,356],{"class":315},[63,1593,1594],{"class":371},"False",[63,1596,1502],{"class":319},[63,1598,1600,1602,1605,1607,1609,1611],{"class":65,"line":1599},24,[63,1601,372],{"class":371},[63,1603,1604],{"class":319},"(report.to_string(",[63,1606,1589],{"class":1400},[63,1608,356],{"class":315},[63,1610,1594],{"class":371},[63,1612,1502],{"class":319},[112,1614,121,1620,121,1623,121,1626,121,1630,121,1633,121,1639,121,1643,121,1647,121,1650,121,1654,121,1657,121,1661,121,1664,121,1668,121,1670,121,1674,121,1676,121,1680,121,1683],{"viewBox":1615,"role":115,"ariaLabelledBy":1616,"xmlns":119,"style":1619},"4 0 744 180",[1617,1618],"lk-outcomes-t","lk-outcomes-d","width:100%;max-width:744px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[123,1621,1622],{"id":1617},"What happened to the eight files in the folder",[127,1624,1625],{"id":1618},"Horizontal bars counting outcomes. Three workbooks were processed, two of them with a warning that they were open elsewhere; one was deferred because it was still being copied; four files were skipped as lock, temporary or non-workbook files.",[131,1627],{"x":213,"y":134,"width":1628,"height":1629,"fill":137},"744","180",[131,1631],{"x":1116,"y":141,"width":1632,"height":143,"rx":265,"fill":144,"stroke":145,"style":146},"201.7",[148,1634,1638],{"x":1635,"y":1636,"style":1637},"120.9","34.5","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","processed",[131,1640],{"x":1641,"y":141,"width":1642,"height":143,"rx":265,"fill":1210,"stroke":1211,"style":160},"231.7","362.8",[148,1644,197],{"x":1645,"y":1636,"style":1646},"602.5","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start",[131,1648],{"x":1116,"y":1649,"width":1632,"height":143,"rx":265,"fill":144,"stroke":145,"style":146},"56.0",[148,1651,1653],{"x":1635,"y":1652,"style":1637},"74.5","…of which open elsewhere",[131,1655],{"x":1641,"y":1649,"width":1656,"height":143,"rx":265,"fill":216,"stroke":217,"style":160},"241.9",[148,1658,180],{"x":1659,"y":1652,"style":1660},"481.6","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start",[131,1662],{"x":1116,"y":1663,"width":1632,"height":143,"rx":265,"fill":144,"stroke":145,"style":146},"96.0",[148,1665,1667],{"x":1635,"y":1666,"style":1637},"114.5","deferred to next run",[131,1669],{"x":1641,"y":1663,"width":1635,"height":143,"rx":265,"fill":158,"stroke":159,"style":160},[148,1671,153],{"x":1672,"y":1666,"style":1673},"360.7","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start",[131,1675],{"x":1116,"y":1111,"width":1632,"height":143,"rx":265,"fill":144,"stroke":145,"style":146},[148,1677,1679],{"x":1635,"y":1678,"style":1637},"154.5","skipped",[131,1681],{"x":1641,"y":1111,"width":1682,"height":143,"rx":265,"fill":144,"stroke":145,"style":160},"483.8",[148,1684,213],{"x":1685,"y":1678,"style":1686},"723.5","font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[10,1688,1689,1690,1693,1694,1698],{},"Three workbooks are combined; South and West are flagged as open elsewhere; East is deferred to the next run; the owner file, lock file, temporary file and text file are skipped with their reasons. Writing ",[14,1691,1692],{},"report"," to the job's log — or into a sheet of the output workbook — means a missing region is explained before anyone asks. ",[43,1695,1697],{"href":1696},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002F","Log Python Excel Script Output to a File"," shows how.",[49,1700,1702],{"id":1701},"performance-notes","Performance notes",[10,1704,1705,1706,1708,1709,1712,1713,1715],{},"Every check here is cheap: filtering is string matching, ",[14,1707,1038],{}," reads the last few kilobytes of a file, and the owner-file test is two ",[14,1710,1711],{},"exists"," calls. The only slow part is the optional settle wait, which sleeps; for a scheduled job that runs every few minutes, skip the wait and rely on ",[14,1714,1038],{}," plus the next run to pick up anything that was mid-copy. On network shares, directory listings can be slow for folders with thousands of files — list once and reuse the result, rather than globbing repeatedly.",[49,1717,1719],{"id":1718},"common-pitfalls","Common pitfalls",[1721,1722,1723,1736,1742,1748,1754],"ul",{},[1724,1725,1726,1732,1733,1735],"li",{},[1727,1728,1729,1731],"strong",{},[14,1730,16],{}," on its own."," It matches ",[14,1734,511],{}," owner files; filter by name first.",[1724,1737,1738,1741],{},[1727,1739,1740],{},"Treating a half-copied file as corrupt."," It is not ready yet; defer it instead of alerting.",[1724,1743,1744,1747],{},[1727,1745,1746],{},"Deleting owner files automatically."," One may belong to a person who has the file open right now; report stale ones instead.",[1724,1749,1750,1753],{},[1727,1751,1752],{},"Stopping on the first bad file."," Handle each file separately and report, so one problem does not hide the rest.",[1724,1755,1756,1759,1760,47],{},[1727,1757,1758],{},"Writing to a workbook someone has open."," On Windows the write fails with a permission error; see ",[43,1761,1763],{"href":1762},"\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",[49,1765,1767],{"id":1766},"conclusion","Conclusion",[10,1769,1770,1771,90,1773,90,1775,1777,1778,1780,1781,1783],{},"Before opening anything, filter the folder by name — ",[14,1772,511],{},[14,1774,830],{},[14,1776,629],{}," and hidden files are not workbooks — and by extension. Check that each remaining ",[14,1779,848],{}," is a complete zip with ",[14,1782,852],{},", optionally after its size has settled, and defer the incomplete ones. Note which workbooks have an owner or lock file beside them, process them as their last saved version with a warning, and report every skip and deferral so the output explains itself.",[49,1785,1787],{"id":1786},"frequently-asked-questions","Frequently asked questions",[10,1789,1790,1793],{},[1727,1791,1792],{},"What is the ~$ file next to my Excel workbook?","\nIt is an owner file that Excel creates while a workbook is open, recording who has it open. It is deleted when the workbook is closed, but can be left behind if Excel crashes. It is not a workbook and cannot be read by pandas or openpyxl.",[10,1795,1796,1799],{},[1727,1797,1798],{},"Why does pandas say \"Excel file format cannot be determined\"?","\nThe file is not a recognisable Excel file — in a folder loop, usually a ~$ owner file that matched *.xlsx. Skip names starting with ~$ before reading.",[10,1801,1802,1805],{},[1727,1803,1804],{},"How can I tell if an Excel file has finished copying?","\nCheck it with zipfile.is_zipfile, which is only true once the whole .xlsx has arrived, and optionally wait until its size stops changing. Leave files that fail for the next run.",[10,1807,1808,1811],{},[1727,1809,1810],{},"Can I read an Excel file while someone has it open?","\nUsually yes: you read the last saved version on disk, which may differ from what they see on screen. Writing to it while it is open will fail on Windows.",[49,1813,1815],{"id":1814},"related","Related",[1721,1817,1818,1824,1831,1838],{},[1724,1819,1820,1821,1823],{},"Up one level: ",[43,1822,46],{"href":45}," — making report jobs robust.",[1724,1825,1826,1830],{},[43,1827,1829],{"href":1828},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-multiple-excel-sheets-in-python\u002Fcombine-multiple-excel-files-into-one-python\u002F","Combine Multiple Excel Files into One"," — the loop this page hardens.",[1724,1832,1833,1837],{},[43,1834,1836],{"href":1835},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fskip-a-report-when-the-input-file-has-not-changed\u002F","Skip a Report When the Input File Has Not Changed"," — another reason to leave a file alone.",[1724,1839,1840,1844],{},[43,1841,1843],{"href":1842},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fprocess-multiple-excel-files-in-parallel-with-python\u002F","Process Multiple Excel Files in Parallel with Python"," — the same filtering before fanning out.",[1846,1847,1848],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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}",{"title":59,"searchDepth":329,"depth":329,"links":1850},[1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861],{"id":51,"depth":329,"text":52},{"id":296,"depth":329,"text":297},{"id":535,"depth":329,"text":536},{"id":841,"depth":329,"text":842},{"id":1228,"depth":329,"text":1229},{"id":1355,"depth":329,"text":1356},{"id":1701,"depth":329,"text":1702},{"id":1718,"depth":329,"text":1719},{"id":1766,"depth":329,"text":1767},{"id":1786,"depth":329,"text":1787},{"id":1814,"depth":329,"text":1815},"2026-09-29","Stop a folder-processing job from crashing on the files Excel and LibreOffice leave next to open workbooks: recognise ~$ owner files, .~lock files and temporary save files, skip half-copied workbooks that are not yet valid zip files, notice when a workbook is open in someone's Excel, and report what was skipped and why.","md",[1866,1868,1870,1872],{"q":1792,"a":1867},"It is an owner file that Excel creates while a workbook is open, recording who has it open. It is deleted when the workbook is closed, but can be left behind if Excel crashes. It is not a workbook and cannot be read by pandas or openpyxl.",{"q":1798,"a":1869},"The file is not a recognisable Excel file — in a folder loop, usually a ~$ owner file that matched *.xlsx. Skip names starting with ~$ before reading.",{"q":1804,"a":1871},"Check it with zipfile.is_zipfile, which is only true once the whole .xlsx has arrived, and optionally wait until its size stops changing. Leave files that fail for the next run.",{"q":1810,"a":1873},"Usually yes: you read the last saved version on disk, which may differ from what they see on screen. Writing to it while it is open will fail on Windows.","ignore excel lock files when processing a folder",null,{"breadcrumb":1877},[1878,1880,1883],{"name":1879,"item":1022},"Home",{"name":1881,"item":1882},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":46,"item":45},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder",{"title":1886,"description":1887},"Skip ~$ Excel Lock Files in a Python Folder Loop","Process a folder of Excel files safely in Python: filter out ~$ owner files, LibreOffice .~lock files and ~WRL temp files, detect half-copied workbooks with zipfile.is_zipfile, wait for files to finish copying, flag workbooks open elsewhere and log every skip.","ignore-excel-lock-files-when-processing-a-folder","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder\u002Findex",[1891,1892],"reliable-jobs","sheets-and-files","how-to","3Evym7NHqocgJDlv1Dnd5cus3VSltUbLRIwQH5kxKjc",[1896,1899],{"title":1763,"path":1897,"stem":1898,"children":-1},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002Findex",{"title":1697,"path":1900,"stem":1901,"children":-1},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","automating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file\u002Findex",[1903,1905],{"path":1904,"title":46},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation",{"path":1906,"title":1881},"\u002Fautomating-reporting-workflows",[],[1909,1913,1916,1919,1922,1925],{"path":1910,"title":1911,"kind":1912},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts","Testing and Packaging Excel Automation Scripts","guide",{"path":1914,"title":1915,"kind":1893},"\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":1917,"title":1918,"kind":1893},"\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":1920,"title":1921,"kind":1893},"\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":1923,"title":1924,"kind":1893},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory","Create Excel Test Fixtures in Memory for pytest",{"path":1926,"title":1927,"kind":1893},"\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",1790972131961]