[{"data":1,"prerenderedAt":1673},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis":3},{"id":4,"title":5,"body":6,"description":1662,"extension":1663,"meta":1664,"navigation":240,"path":1669,"seo":1670,"stem":1671,"__hash__":1672},"docs\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis\u002Findex.md","Run an Algorithm Over a Folder of Files in PyQGIS",{"type":7,"value":8,"toc":1648},"minimark",[9,13,17,26,176,181,203,207,424,438,442,600,609,613,806,820,911,915,1051,1060,1064,1067,1202,1215,1293,1297,1300,1397,1410,1414,1482,1497,1501,1546,1550,1557,1561,1567,1573,1587,1593,1599,1613,1617,1644],[10,11,5],"h1",{"id":12},"run-an-algorithm-over-a-folder-of-files-in-pyqgis",[14,15,16],"p",{},"The Processing toolbox has a batch mode, and for a one-off run over twenty files it is exactly right. For anything that repeats — a nightly conversion, a monthly reproject, the same treatment applied to whatever a supplier delivered this week — a script is better, because it can discover its own inputs, skip what it has already done, survive a bad file, and tell you what happened.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002F","Batch Processing with PyQGIS",". It covers finding the inputs, running an algorithm per file, error handling that does not abandon the run, output naming, and the summary that makes an unattended job trustworthy.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,97,100,105,113,118,122,126,131,134,138,142,150,154,158,162,168,172],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 274","img","Diagram of a batch run over a folder showing files discovered, each processed independently, with successes written to the output folder and failures recorded without stopping the run","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"One bad file should not end the run",[40,41,42],"desc",{},"The script discovers the input files, then processes each one independently. Successful files are written to the output folder. A file that fails is recorded with its error and the loop continues to the next one. At the end the script reports how many succeeded, how many were skipped as already done, and which ones failed.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","274","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"batchArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Isolate each file, then summarise",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"20","66","164","140","10","#eef7f4","#0f766e","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"102","92","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","discover",[68,93,96],{"x":88,"y":94,"style":95,"fill":66,"textAnchor":74},"118","text-anchor:middle;font-size:11px;font-family:sans-serif","42 shapefiles",[68,98,99],{"x":88,"y":81,"style":95,"fill":66,"textAnchor":74},"sorted, deduplicated",[68,101,104],{"x":88,"y":102,"style":95,"fill":103,"textAnchor":74},"166","#59645f","glob, not a hard-coded list",[44,106],{"x":107,"y":108,"width":109,"height":108,"rx":58,"fill":110,"stroke":111,"style":112},"248","52","252","#fffdf7","#15803d","stroke-width:2",[68,114,117],{"x":115,"y":116,"style":95,"fill":66,"textAnchor":74},"374","82","file 1 — reprojected, written",[44,119],{"x":107,"y":120,"width":109,"height":108,"rx":58,"fill":110,"stroke":121,"style":112},"112","#b91c1c",[68,123,125],{"x":115,"y":124,"style":95,"fill":66,"textAnchor":74},"136","file 2 — invalid geometry",[68,127,130],{"x":115,"y":128,"style":129,"fill":121,"textAnchor":74},"154","text-anchor:middle;font-size:10px;font-family:sans-serif","recorded, run continues",[44,132],{"x":107,"y":133,"width":109,"height":108,"rx":58,"fill":110,"stroke":103,"style":112},"172",[68,135,137],{"x":115,"y":136,"style":95,"fill":66,"textAnchor":74},"196","file 3 — output already exists",[68,139,141],{"x":115,"y":140,"style":129,"fill":103,"textAnchor":74},"214","skipped",[44,143],{"x":144,"y":145,"width":146,"height":147,"rx":82,"fill":148,"stroke":149,"style":85},"548","90","192","96","#eff3ff","#2563eb",[68,151,153],{"x":152,"y":94,"style":90,"fill":149,"textAnchor":74},"644","summary",[68,155,157],{"x":152,"y":156,"style":95,"fill":66,"textAnchor":74},"142","39 done, 1 failed",[68,159,161],{"x":152,"y":160,"style":95,"fill":66,"textAnchor":74},"162","2 skipped, named",[163,164],"line",{"x1":165,"y1":124,"x2":166,"y2":124,"stroke":66,"style":167},"184","242","stroke-width:2;marker-end:url(#batchArrow)",[163,169],{"x1":170,"y1":124,"x2":171,"y2":124,"stroke":66,"style":167},"500","542",[68,173,175],{"x":70,"y":174,"style":95,"fill":103,"textAnchor":74},"256","A run that stops at the first bad file finishes 3 of 42 and reports failure",[177,178,180],"h2",{"id":179},"prerequisites","Prerequisites",[182,183,184,192,195],"ul",{},[185,186,187,191],"li",{},[188,189,190],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[185,193,194],{},"A folder of inputs and a folder for outputs.",[185,196,197,198,202],{},"Familiarity with running one algorithm — see ",[21,199,201],{"href":200},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-processing-algorithm-from-script\u002F","Run a Processing Algorithm from a Script",".",[177,204,206],{"id":205},"discover-the-inputs","Discover the inputs",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-python shiki shiki-themes github-dark","from pathlib import Path\n\nINPUT_DIR = Path(\"\u002Fdata\u002Fdeliveries\u002F2026-08\")\nOUTPUT_DIR = Path(\"\u002Fdata\u002Fprocessed\u002F2026-08\")\nPATTERNS = (\"*.shp\", \"*.gpkg\", \"*.geojson\")\n\ndef find_inputs(folder):\n    files = []\n    for pattern in PATTERNS:\n        files.extend(folder.glob(pattern))\n    return sorted(set(files))\n\ninputs = find_inputs(INPUT_DIR)\nprint(f\"{len(inputs)} files to process\")\n","python","",[215,216,217,235,242,262,277,304,309,322,334,352,358,376,381,396],"code",{"__ignoreMap":213},[218,219,221,225,229,232],"span",{"class":163,"line":220},1,[218,222,224],{"class":223},"snl16","from",[218,226,228],{"class":227},"s95oV"," pathlib ",[218,230,231],{"class":223},"import",[218,233,234],{"class":227}," Path\n",[218,236,238],{"class":163,"line":237},2,[218,239,241],{"emptyLinePlaceholder":240},true,"\n",[218,243,245,249,252,255,259],{"class":163,"line":244},3,[218,246,248],{"class":247},"sDLfK","INPUT_DIR",[218,250,251],{"class":223}," =",[218,253,254],{"class":227}," Path(",[218,256,258],{"class":257},"sU2Wk","\"\u002Fdata\u002Fdeliveries\u002F2026-08\"",[218,260,261],{"class":227},")\n",[218,263,265,268,270,272,275],{"class":163,"line":264},4,[218,266,267],{"class":247},"OUTPUT_DIR",[218,269,251],{"class":223},[218,271,254],{"class":227},[218,273,274],{"class":257},"\"\u002Fdata\u002Fprocessed\u002F2026-08\"",[218,276,261],{"class":227},[218,278,280,283,285,288,291,294,297,299,302],{"class":163,"line":279},5,[218,281,282],{"class":247},"PATTERNS",[218,284,251],{"class":223},[218,286,287],{"class":227}," (",[218,289,290],{"class":257},"\"*.shp\"",[218,292,293],{"class":227},", ",[218,295,296],{"class":257},"\"*.gpkg\"",[218,298,293],{"class":227},[218,300,301],{"class":257},"\"*.geojson\"",[218,303,261],{"class":227},[218,305,307],{"class":163,"line":306},6,[218,308,241],{"emptyLinePlaceholder":240},[218,310,312,315,319],{"class":163,"line":311},7,[218,313,314],{"class":223},"def",[218,316,318],{"class":317},"svObZ"," find_inputs",[218,320,321],{"class":227},"(folder):\n",[218,323,325,328,331],{"class":163,"line":324},8,[218,326,327],{"class":227},"    files ",[218,329,330],{"class":223},"=",[218,332,333],{"class":227}," []\n",[218,335,337,340,343,346,349],{"class":163,"line":336},9,[218,338,339],{"class":223},"    for",[218,341,342],{"class":227}," pattern ",[218,344,345],{"class":223},"in",[218,347,348],{"class":247}," PATTERNS",[218,350,351],{"class":227},":\n",[218,353,355],{"class":163,"line":354},10,[218,356,357],{"class":227},"        files.extend(folder.glob(pattern))\n",[218,359,361,364,367,370,373],{"class":163,"line":360},11,[218,362,363],{"class":223},"    return",[218,365,366],{"class":247}," sorted",[218,368,369],{"class":227},"(",[218,371,372],{"class":247},"set",[218,374,375],{"class":227},"(files))\n",[218,377,379],{"class":163,"line":378},12,[218,380,241],{"emptyLinePlaceholder":240},[218,382,384,387,389,392,394],{"class":163,"line":383},13,[218,385,386],{"class":227},"inputs ",[218,388,330],{"class":223},[218,390,391],{"class":227}," find_inputs(",[218,393,248],{"class":247},[218,395,261],{"class":227},[218,397,399,402,404,407,410,413,416,419,422],{"class":163,"line":398},14,[218,400,401],{"class":247},"print",[218,403,369],{"class":227},[218,405,406],{"class":223},"f",[218,408,409],{"class":257},"\"",[218,411,412],{"class":247},"{len",[218,414,415],{"class":227},"(inputs)",[218,417,418],{"class":247},"}",[218,420,421],{"class":257}," files to process\"",[218,423,261],{"class":227},[14,425,426,429,430,433,434,437],{},[188,427,428],{},"Breakdown:"," Globbing rather than hard-coding a list means the script handles whatever arrived this month, which is the entire point of a batch job. Sorting makes the run reproducible and the log readable; deduplicating matters because overlapping patterns can match the same file twice on case-insensitive file systems. Use ",[215,431,432],{},"rglob()"," instead of ",[215,435,436],{},"glob()"," when deliveries arrive in subfolders — but be deliberate about it, since a recursive sweep of a shared drive can pick up an archive folder nobody meant to reprocess. Printing the count first is a cheap guard: a count of zero means the pattern or the path is wrong, and finding that out now beats finding it out after a silent, empty run.",[177,439,441],{"id":440},"process-each-file-independently","Process each file independently",[208,443,445],{"className":210,"code":444,"language":212,"meta":213,"style":213},"import processing\nfrom qgis.core import QgsVectorLayer\n\ndef process_one(path, output_path):\n    layer = QgsVectorLayer(str(path), path.stem, \"ogr\")\n    if not layer.isValid():\n        raise RuntimeError(f\"cannot open: {layer.error().summary()}\")\n\n    processing.run(\"native:reprojectlayer\", {\n        \"INPUT\": layer,\n        \"TARGET_CRS\": \"EPSG:27700\",\n        \"OUTPUT\": str(output_path),\n    })\n    return output_path\n",[215,446,447,454,466,470,480,501,512,539,543,554,562,576,588,593],{"__ignoreMap":213},[218,448,449,451],{"class":163,"line":220},[218,450,231],{"class":223},[218,452,453],{"class":227}," processing\n",[218,455,456,458,461,463],{"class":163,"line":237},[218,457,224],{"class":223},[218,459,460],{"class":227}," qgis.core ",[218,462,231],{"class":223},[218,464,465],{"class":227}," QgsVectorLayer\n",[218,467,468],{"class":163,"line":244},[218,469,241],{"emptyLinePlaceholder":240},[218,471,472,474,477],{"class":163,"line":264},[218,473,314],{"class":223},[218,475,476],{"class":317}," process_one",[218,478,479],{"class":227},"(path, output_path):\n",[218,481,482,485,487,490,493,496,499],{"class":163,"line":279},[218,483,484],{"class":227},"    layer ",[218,486,330],{"class":223},[218,488,489],{"class":227}," QgsVectorLayer(",[218,491,492],{"class":247},"str",[218,494,495],{"class":227},"(path), path.stem, ",[218,497,498],{"class":257},"\"ogr\"",[218,500,261],{"class":227},[218,502,503,506,509],{"class":163,"line":306},[218,504,505],{"class":223},"    if",[218,507,508],{"class":223}," not",[218,510,511],{"class":227}," layer.isValid():\n",[218,513,514,517,520,522,524,527,530,533,535,537],{"class":163,"line":311},[218,515,516],{"class":223},"        raise",[218,518,519],{"class":247}," RuntimeError",[218,521,369],{"class":227},[218,523,406],{"class":223},[218,525,526],{"class":257},"\"cannot open: ",[218,528,529],{"class":247},"{",[218,531,532],{"class":227},"layer.error().summary()",[218,534,418],{"class":247},[218,536,409],{"class":257},[218,538,261],{"class":227},[218,540,541],{"class":163,"line":324},[218,542,241],{"emptyLinePlaceholder":240},[218,544,545,548,551],{"class":163,"line":336},[218,546,547],{"class":227},"    processing.run(",[218,549,550],{"class":257},"\"native:reprojectlayer\"",[218,552,553],{"class":227},", {\n",[218,555,556,559],{"class":163,"line":354},[218,557,558],{"class":257},"        \"INPUT\"",[218,560,561],{"class":227},": layer,\n",[218,563,564,567,570,573],{"class":163,"line":360},[218,565,566],{"class":257},"        \"TARGET_CRS\"",[218,568,569],{"class":227},": ",[218,571,572],{"class":257},"\"EPSG:27700\"",[218,574,575],{"class":227},",\n",[218,577,578,581,583,585],{"class":163,"line":378},[218,579,580],{"class":257},"        \"OUTPUT\"",[218,582,569],{"class":227},[218,584,492],{"class":247},[218,586,587],{"class":227},"(output_path),\n",[218,589,590],{"class":163,"line":383},[218,591,592],{"class":227},"    })\n",[218,594,595,597],{"class":163,"line":398},[218,596,363],{"class":223},[218,598,599],{"class":227}," output_path\n",[14,601,602,604,605,608],{},[188,603,428],{}," Keeping the per-file work in its own function is what makes the error handling clean — everything that can go wrong for one file raises, and the caller decides what that means for the run. Passing the layer object rather than the path to ",[215,606,607],{},"processing.run()"," means the validity check has already happened, so a corrupt file fails with your message rather than the algorithm's more cryptic one. Writing to an explicit output path rather than a temporary one is what makes the run resumable, because the file on disk is the record of what has been done.",[177,610,612],{"id":611},"keep-going-when-one-fails","Keep going when one fails",[208,614,616],{"className":210,"code":615,"language":212,"meta":213,"style":213},"def run_batch(inputs, output_dir, skip_existing=True):\n    output_dir.mkdir(parents=True, exist_ok=True)\n    done, skipped, failed = [], [], []\n\n    for path in inputs:\n        output_path = output_dir \u002F f\"{path.stem}_bng.gpkg\"\n\n        if skip_existing and output_path.exists():\n            skipped.append(path.name)\n            continue\n\n        try:\n            process_one(path, output_path)\n            done.append(path.name)\n        except Exception as error:                  # noqa: BLE001 — per-file isolation\n            failed.append((path.name, str(error)))\n\n    return done, skipped, failed\n",[215,617,618,636,660,670,674,686,714,718,732,737,742,746,753,758,763,782,793,798],{"__ignoreMap":213},[218,619,620,622,625,628,630,633],{"class":163,"line":220},[218,621,314],{"class":223},[218,623,624],{"class":317}," run_batch",[218,626,627],{"class":227},"(inputs, output_dir, skip_existing",[218,629,330],{"class":223},[218,631,632],{"class":247},"True",[218,634,635],{"class":227},"):\n",[218,637,638,641,645,647,649,651,654,656,658],{"class":163,"line":237},[218,639,640],{"class":227},"    output_dir.mkdir(",[218,642,644],{"class":643},"s9osk","parents",[218,646,330],{"class":223},[218,648,632],{"class":247},[218,650,293],{"class":227},[218,652,653],{"class":643},"exist_ok",[218,655,330],{"class":223},[218,657,632],{"class":247},[218,659,261],{"class":227},[218,661,662,665,667],{"class":163,"line":244},[218,663,664],{"class":227},"    done, skipped, failed ",[218,666,330],{"class":223},[218,668,669],{"class":227}," [], [], []\n",[218,671,672],{"class":163,"line":264},[218,673,241],{"emptyLinePlaceholder":240},[218,675,676,678,681,683],{"class":163,"line":279},[218,677,339],{"class":223},[218,679,680],{"class":227}," path ",[218,682,345],{"class":223},[218,684,685],{"class":227}," inputs:\n",[218,687,688,691,693,696,699,702,704,706,709,711],{"class":163,"line":306},[218,689,690],{"class":227},"        output_path ",[218,692,330],{"class":223},[218,694,695],{"class":227}," output_dir ",[218,697,698],{"class":223},"\u002F",[218,700,701],{"class":223}," f",[218,703,409],{"class":257},[218,705,529],{"class":247},[218,707,708],{"class":227},"path.stem",[218,710,418],{"class":247},[218,712,713],{"class":257},"_bng.gpkg\"\n",[218,715,716],{"class":163,"line":311},[218,717,241],{"emptyLinePlaceholder":240},[218,719,720,723,726,729],{"class":163,"line":324},[218,721,722],{"class":223},"        if",[218,724,725],{"class":227}," skip_existing ",[218,727,728],{"class":223},"and",[218,730,731],{"class":227}," output_path.exists():\n",[218,733,734],{"class":163,"line":336},[218,735,736],{"class":227},"            skipped.append(path.name)\n",[218,738,739],{"class":163,"line":354},[218,740,741],{"class":223},"            continue\n",[218,743,744],{"class":163,"line":360},[218,745,241],{"emptyLinePlaceholder":240},[218,747,748,751],{"class":163,"line":378},[218,749,750],{"class":223},"        try",[218,752,351],{"class":227},[218,754,755],{"class":163,"line":383},[218,756,757],{"class":227},"            process_one(path, output_path)\n",[218,759,760],{"class":163,"line":398},[218,761,762],{"class":227},"            done.append(path.name)\n",[218,764,766,769,772,775,778],{"class":163,"line":765},15,[218,767,768],{"class":223},"        except",[218,770,771],{"class":247}," Exception",[218,773,774],{"class":223}," as",[218,776,777],{"class":227}," error:                  ",[218,779,781],{"class":780},"sjoCn","# noqa: BLE001 — per-file isolation\n",[218,783,785,788,790],{"class":163,"line":784},16,[218,786,787],{"class":227},"            failed.append((path.name, ",[218,789,492],{"class":247},[218,791,792],{"class":227},"(error)))\n",[218,794,796],{"class":163,"line":795},17,[218,797,241],{"emptyLinePlaceholder":240},[218,799,801,803],{"class":163,"line":800},18,[218,802,363],{"class":223},[218,804,805],{"class":227}," done, skipped, failed\n",[14,807,808,810,811,814,815,819],{},[188,809,428],{}," The broad ",[215,812,813],{},"except"," is justified here and almost nowhere else: the unit of failure is one file, and abandoning forty because the third is corrupt helps nobody. What makes it acceptable is that the error is ",[816,817,818],"em",{},"recorded with the file name"," rather than swallowed, so nothing is lost. Skipping existing outputs turns the script into something you can re-run after fixing one file, which is how batch jobs are actually used. Creating the output directory once, before the loop, avoids a per-iteration check and produces a clearer error if the path is not writable.",[14,821,822],{},[29,823,826,829,832,835,838,844,849,856,862,865,870,873,878,882,886,890,893,897,900,903,905,908],{"viewBox":824,"role":32,"ariaLabel":825,"xmlns":34},"0 0 760 250","Comparison of a batch loop that stops at the first exception against one that records each failure and continues, showing the outcome after a run over forty files",[36,827,828],{},"Two loops, forty files, one bad input",[40,830,831],{},"A loop without per-file error handling stops at the third file, leaving thirty-nine unprocessed and a traceback that names one problem. A loop that catches per file processes thirty-nine successfully, records the one failure with its message, and finishes, so the operator fixes one file rather than rerunning everything.",[44,833],{"x":46,"y":46,"width":47,"height":834,"fill":49},"250",[68,836,837],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"The same bad file, two very different mornings",[44,839],{"x":78,"y":840,"width":841,"height":842,"rx":82,"fill":843,"stroke":121,"style":85},"48","348","180","#fdf2e2",[68,845,848],{"x":846,"y":847,"style":90,"fill":121,"textAnchor":74},"194","74","no per-file handling",[44,850],{"x":851,"y":145,"width":852,"height":853,"rx":854,"fill":110,"stroke":111,"style":855},"44","300","30","6","stroke-width:1.5",[68,857,861],{"x":858,"y":859,"style":860,"fill":66},"60","110","font-size:11px;font-family:sans-serif","files 1 and 2 — done",[44,863],{"x":851,"y":864,"width":852,"height":853,"rx":854,"fill":110,"stroke":121,"style":112},"126",[68,866,869],{"x":858,"y":867,"style":868,"fill":121},"146","font-size:11px;font-weight:bold;font-family:sans-serif","file 3 — exception, loop ends",[44,871],{"x":851,"y":160,"width":852,"height":872,"rx":854,"fill":49,"stroke":121,"style":112},"46",[68,874,877],{"x":846,"y":875,"style":876,"fill":121,"textAnchor":74},"182","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","37 files untouched",[68,879,881],{"x":846,"y":880,"style":129,"fill":66,"textAnchor":74},"200","and the run reports failure",[44,883],{"x":884,"y":840,"width":841,"height":842,"rx":82,"fill":885,"stroke":111,"style":85},"392","#edf8e9",[68,887,889],{"x":888,"y":847,"style":90,"fill":111,"textAnchor":74},"566","caught per file",[44,891],{"x":892,"y":145,"width":852,"height":853,"rx":854,"fill":110,"stroke":111,"style":855},"416",[68,894,896],{"x":895,"y":859,"style":860,"fill":66},"432","39 files processed",[44,898],{"x":892,"y":864,"width":852,"height":853,"rx":854,"fill":110,"stroke":899,"style":855},"#b45309",[68,901,902],{"x":895,"y":867,"style":860,"fill":66},"file 3 recorded with its error",[44,904],{"x":892,"y":160,"width":852,"height":872,"rx":854,"fill":110,"stroke":111,"style":112},[68,906,907],{"x":888,"y":875,"style":876,"fill":111,"textAnchor":74},"fix one file, re-run",[68,909,910],{"x":888,"y":880,"style":129,"fill":66,"textAnchor":74},"the rest are skipped as done",[177,912,914],{"id":913},"report-what-happened","Report what happened",[208,916,918],{"className":210,"code":917,"language":212,"meta":213,"style":213},"done, skipped, failed = run_batch(inputs, OUTPUT_DIR)\n\nprint(f\"processed {len(done)}, skipped {len(skipped)}, failed {len(failed)}\")\nfor name, error in failed:\n    print(f\"  FAILED {name}: {error}\")\n\nif failed:\n    raise SystemExit(1)\n",[215,919,920,934,938,980,993,1025,1029,1036],{"__ignoreMap":213},[218,921,922,925,927,930,932],{"class":163,"line":220},[218,923,924],{"class":227},"done, skipped, failed ",[218,926,330],{"class":223},[218,928,929],{"class":227}," run_batch(inputs, ",[218,931,267],{"class":247},[218,933,261],{"class":227},[218,935,936],{"class":163,"line":237},[218,937,241],{"emptyLinePlaceholder":240},[218,939,940,942,944,946,949,951,954,956,959,961,964,966,969,971,974,976,978],{"class":163,"line":244},[218,941,401],{"class":247},[218,943,369],{"class":227},[218,945,406],{"class":223},[218,947,948],{"class":257},"\"processed ",[218,950,412],{"class":247},[218,952,953],{"class":227},"(done)",[218,955,418],{"class":247},[218,957,958],{"class":257},", skipped ",[218,960,412],{"class":247},[218,962,963],{"class":227},"(skipped)",[218,965,418],{"class":247},[218,967,968],{"class":257},", failed ",[218,970,412],{"class":247},[218,972,973],{"class":227},"(failed)",[218,975,418],{"class":247},[218,977,409],{"class":257},[218,979,261],{"class":227},[218,981,982,985,988,990],{"class":163,"line":264},[218,983,984],{"class":223},"for",[218,986,987],{"class":227}," name, error ",[218,989,345],{"class":223},[218,991,992],{"class":227}," failed:\n",[218,994,995,998,1000,1002,1005,1007,1010,1012,1014,1016,1019,1021,1023],{"class":163,"line":279},[218,996,997],{"class":247},"    print",[218,999,369],{"class":227},[218,1001,406],{"class":223},[218,1003,1004],{"class":257},"\"  FAILED ",[218,1006,529],{"class":247},[218,1008,1009],{"class":227},"name",[218,1011,418],{"class":247},[218,1013,569],{"class":257},[218,1015,529],{"class":247},[218,1017,1018],{"class":227},"error",[218,1020,418],{"class":247},[218,1022,409],{"class":257},[218,1024,261],{"class":227},[218,1026,1027],{"class":163,"line":306},[218,1028,241],{"emptyLinePlaceholder":240},[218,1030,1031,1034],{"class":163,"line":311},[218,1032,1033],{"class":223},"if",[218,1035,992],{"class":227},[218,1037,1038,1041,1044,1046,1049],{"class":163,"line":324},[218,1039,1040],{"class":223},"    raise",[218,1042,1043],{"class":247}," SystemExit",[218,1045,369],{"class":227},[218,1047,1048],{"class":247},"1",[218,1050,261],{"class":227},[14,1052,1053,1055,1056,202],{},[188,1054,428],{}," Three numbers and a list of failures is the whole report, and it is what makes an unattended run trustworthy — a job that prints nothing tells you only that it did not crash. Exiting non-zero when anything failed is what lets a scheduler notice; a job that always exits zero is invisible to monitoring, which is the practical difference between a problem found tonight and one found next quarter. Where the run feeds something downstream, record the counts somewhere durable as well, following the logging patterns in ",[21,1057,1059],{"href":1058},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fhandle-errors-and-logging-in-unattended-scripts\u002F","Handle Errors and Logging in Unattended Scripts",[177,1061,1063],{"id":1062},"handle-multi-layer-containers","Handle multi-layer containers",[14,1065,1066],{},"A folder of GeoPackages is not a folder of layers — each file may hold several, and globbing gives you the container rather than its contents.",[208,1068,1070],{"className":210,"code":1069,"language":212,"meta":213,"style":213},"from qgis.core import QgsProviderRegistry\n\ndef layers_in(path):\n    if path.suffix.lower() != \".gpkg\":\n        return [(str(path), path.stem)]\n\n    metadata = QgsProviderRegistry.instance().providerMetadata(\"ogr\")\n    connection = metadata.createConnection(str(path), {})\n    return [(f\"{path}|layername={table.tableName()}\", table.tableName())\n            for table in connection.tables()]\n",[215,1071,1072,1083,1087,1097,1112,1125,1129,1143,1158,1189],{"__ignoreMap":213},[218,1073,1074,1076,1078,1080],{"class":163,"line":220},[218,1075,224],{"class":223},[218,1077,460],{"class":227},[218,1079,231],{"class":223},[218,1081,1082],{"class":227}," QgsProviderRegistry\n",[218,1084,1085],{"class":163,"line":237},[218,1086,241],{"emptyLinePlaceholder":240},[218,1088,1089,1091,1094],{"class":163,"line":244},[218,1090,314],{"class":223},[218,1092,1093],{"class":317}," layers_in",[218,1095,1096],{"class":227},"(path):\n",[218,1098,1099,1101,1104,1107,1110],{"class":163,"line":264},[218,1100,505],{"class":223},[218,1102,1103],{"class":227}," path.suffix.lower() ",[218,1105,1106],{"class":223},"!=",[218,1108,1109],{"class":257}," \".gpkg\"",[218,1111,351],{"class":227},[218,1113,1114,1117,1120,1122],{"class":163,"line":279},[218,1115,1116],{"class":223},"        return",[218,1118,1119],{"class":227}," [(",[218,1121,492],{"class":247},[218,1123,1124],{"class":227},"(path), path.stem)]\n",[218,1126,1127],{"class":163,"line":306},[218,1128,241],{"emptyLinePlaceholder":240},[218,1130,1131,1134,1136,1139,1141],{"class":163,"line":311},[218,1132,1133],{"class":227},"    metadata ",[218,1135,330],{"class":223},[218,1137,1138],{"class":227}," QgsProviderRegistry.instance().providerMetadata(",[218,1140,498],{"class":257},[218,1142,261],{"class":227},[218,1144,1145,1148,1150,1153,1155],{"class":163,"line":324},[218,1146,1147],{"class":227},"    connection ",[218,1149,330],{"class":223},[218,1151,1152],{"class":227}," metadata.createConnection(",[218,1154,492],{"class":247},[218,1156,1157],{"class":227},"(path), {})\n",[218,1159,1160,1162,1164,1166,1168,1170,1172,1174,1177,1179,1182,1184,1186],{"class":163,"line":336},[218,1161,363],{"class":223},[218,1163,1119],{"class":227},[218,1165,406],{"class":223},[218,1167,409],{"class":257},[218,1169,529],{"class":247},[218,1171,63],{"class":227},[218,1173,418],{"class":247},[218,1175,1176],{"class":257},"|layername=",[218,1178,529],{"class":247},[218,1180,1181],{"class":227},"table.tableName()",[218,1183,418],{"class":247},[218,1185,409],{"class":257},[218,1187,1188],{"class":227},", table.tableName())\n",[218,1190,1191,1194,1197,1199],{"class":163,"line":354},[218,1192,1193],{"class":223},"            for",[218,1195,1196],{"class":227}," table ",[218,1198,345],{"class":223},[218,1200,1201],{"class":227}," connection.tables()]\n",[14,1203,1204,1206,1207,1210,1211,202],{},[188,1205,428],{}," The provider connection API lists the layers inside a container without loading any of them, which is both faster and more reliable than parsing the file. Returning a list of source-string and name pairs lets the caller treat single-layer and multi-layer inputs identically — the single-file case returns a one-element list rather than a special case in the loop. Note that a GeoPackage can also contain rasters and non-spatial tables, so filter on ",[215,1208,1209],{},"table.geometryColumnTypes()"," when the algorithm needs geometry. The container-versus-layer distinction is covered further in ",[21,1212,1214],{"href":1213},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fwrite-vector-layer-to-geopackage-pyqgis\u002F","Write a Vector Layer to GeoPackage in PyQGIS",[14,1216,1217],{},[29,1218,1221,1224,1227,1230,1237,1240,1243,1247,1251,1255,1258,1262,1265,1268,1270,1273,1275,1278,1280,1283,1285,1288],{"viewBox":1219,"role":32,"ariaLabel":1220,"xmlns":34},"0 0 760 246","Diagram contrasting a folder of single-layer shapefiles with a folder of GeoPackages each holding several layers, and the different iteration each requires",[36,1222,1223],{},"A folder of files is not a list of layers",[40,1225,1226],{},"Each shapefile in a folder is one layer, so iterating files and iterating layers are the same thing. Each GeoPackage may hold several layers plus rasters and non-spatial tables, so a batch that iterates files processes only the first layer of each container unless it enumerates the contents first.",[44,1228],{"x":46,"y":46,"width":47,"height":1229,"fill":49},"246",[51,1231,1232],{},[54,1233,1235],{"id":1234,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"contArrow",[63,1236],{"d":65,"fill":84},[68,1238,1239],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Count layers, not files",[44,1241],{"x":78,"y":108,"width":841,"height":1242,"rx":82,"fill":885,"stroke":111,"style":85},"176",[68,1244,1246],{"x":846,"y":1245,"style":90,"fill":111,"textAnchor":74},"78","shapefiles — one each",[44,1248],{"x":851,"y":1249,"width":852,"height":853,"rx":59,"fill":110,"stroke":103,"style":1250},"94","stroke-width:1.2",[68,1252,1254],{"x":858,"y":1253,"style":860,"fill":66},"114","roads.shp — 1 layer",[44,1256],{"x":851,"y":1257,"width":852,"height":853,"rx":59,"fill":110,"stroke":103,"style":1250},"130",[68,1259,1261],{"x":858,"y":1260,"style":860,"fill":66},"150","parcels.shp — 1 layer",[44,1263],{"x":851,"y":102,"width":852,"height":1264,"rx":59,"fill":110,"stroke":111,"style":112},"42",[68,1266,1267],{"x":846,"y":146,"style":876,"fill":111,"textAnchor":74},"iterating files is enough",[44,1269],{"x":884,"y":108,"width":841,"height":1242,"rx":82,"fill":843,"stroke":899,"style":85},[68,1271,1272],{"x":888,"y":1245,"style":90,"fill":899,"textAnchor":74},"GeoPackages — several each",[44,1274],{"x":892,"y":1249,"width":852,"height":853,"rx":59,"fill":110,"stroke":103,"style":1250},[68,1276,1277],{"x":895,"y":1253,"style":860,"fill":66},"city.gpkg — roads, parcels, trees",[44,1279],{"x":892,"y":1257,"width":852,"height":853,"rx":59,"fill":110,"stroke":103,"style":1250},[68,1281,1282],{"x":895,"y":1260,"style":860,"fill":66},"survey.gpkg — points, notes table",[44,1284],{"x":892,"y":102,"width":852,"height":1264,"rx":59,"fill":110,"stroke":899,"style":112},[68,1286,1287],{"x":888,"y":146,"style":876,"fill":899,"textAnchor":74},"enumerate the contents first",[163,1289],{"x1":1290,"y1":81,"x2":1291,"y2":81,"stroke":84,"style":1292},"368","386","stroke-width:2;marker-end:url(#contArrow)",[177,1294,1296],{"id":1295},"keep-the-run-observable-while-it-works","Keep the run observable while it works",[14,1298,1299],{},"For a run over hundreds of files, print progress as it goes rather than only at the end:",[208,1301,1303],{"className":210,"code":1302,"language":212,"meta":213,"style":213},"total = len(inputs)\nfor index, path in enumerate(inputs, start=1):\n    print(f\"[{index}\u002F{total}] {path.name}\", flush=True)\n    ...\n",[215,1304,1305,1318,1342,1392],{"__ignoreMap":213},[218,1306,1307,1310,1312,1315],{"class":163,"line":220},[218,1308,1309],{"class":227},"total ",[218,1311,330],{"class":223},[218,1313,1314],{"class":247}," len",[218,1316,1317],{"class":227},"(inputs)\n",[218,1319,1320,1322,1325,1327,1330,1333,1336,1338,1340],{"class":163,"line":237},[218,1321,984],{"class":223},[218,1323,1324],{"class":227}," index, path ",[218,1326,345],{"class":223},[218,1328,1329],{"class":247}," enumerate",[218,1331,1332],{"class":227},"(inputs, ",[218,1334,1335],{"class":643},"start",[218,1337,330],{"class":223},[218,1339,1048],{"class":247},[218,1341,635],{"class":227},[218,1343,1344,1346,1348,1350,1353,1355,1358,1360,1362,1364,1367,1369,1372,1374,1377,1379,1381,1383,1386,1388,1390],{"class":163,"line":244},[218,1345,997],{"class":247},[218,1347,369],{"class":227},[218,1349,406],{"class":223},[218,1351,1352],{"class":257},"\"[",[218,1354,529],{"class":247},[218,1356,1357],{"class":227},"index",[218,1359,418],{"class":247},[218,1361,698],{"class":257},[218,1363,529],{"class":247},[218,1365,1366],{"class":227},"total",[218,1368,418],{"class":247},[218,1370,1371],{"class":257},"] ",[218,1373,529],{"class":247},[218,1375,1376],{"class":227},"path.name",[218,1378,418],{"class":247},[218,1380,409],{"class":257},[218,1382,293],{"class":227},[218,1384,1385],{"class":643},"flush",[218,1387,330],{"class":223},[218,1389,632],{"class":247},[218,1391,261],{"class":227},[218,1393,1394],{"class":163,"line":264},[218,1395,1396],{"class":247},"    ...\n",[14,1398,1399,1401,1402,1405,1406,202],{},[188,1400,428],{}," The counter plus the file name is enough to tell an operator both how far along the run is and which file it is on when it appears to hang — which is nearly always a single pathological input rather than a general slowdown. ",[215,1403,1404],{},"flush=True"," matters when the output is redirected to a log file, because Python buffers otherwise and the log stays empty until the process ends, which is exactly the moment you no longer need it. In a plugin, the same information belongs on a progress bar and a background task, as covered in ",[21,1407,1409],{"href":1408},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fshow-plugin-progress-and-cancellation-pyqgis\u002F","Show Plugin Progress and Cancellation in PyQGIS",[177,1411,1413],{"id":1412},"qgis-version-compatibility","QGIS version compatibility",[1415,1416,1417,1433],"table",{},[1418,1419,1420],"thead",{},[1421,1422,1423,1427,1430],"tr",{},[1424,1425,1426],"th",{},"QGIS version",[1424,1428,1429],{},"Python",[1424,1431,1432],{},"Notes",[1434,1435,1436,1451,1461,1472],"tbody",{},[1421,1437,1438,1442,1445],{},[1439,1440,1441],"td",{},"3.22 LTR",[1439,1443,1444],{},"3.9",[1439,1446,1447,1450],{},[215,1448,1449],{},"processing.run",", provider connections and everything shown behave identically.",[1421,1452,1453,1456,1458],{},[1439,1454,1455],{},"3.28 LTR",[1439,1457,1444],{},[1439,1459,1460],{},"Identical.",[1421,1462,1463,1466,1469],{},[1439,1464,1465],{},"3.34 LTR",[1439,1467,1468],{},"3.12",[1439,1470,1471],{},"Baseline for this page.",[1421,1473,1474,1477,1479],{},[1439,1475,1476],{},"3.40 \u002F 3.44",[1439,1478,1468],{},[1439,1480,1481],{},"Identical; algorithm names are stable, though parameter defaults occasionally change between releases.",[14,1483,1484,1485,1488,1489,1492,1493,1496],{},"Algorithm identifiers such as ",[215,1486,1487],{},"native:reprojectlayer"," are stable across the 3.x series; the ",[215,1490,1491],{},"qgis:"," prefixed Python implementations of older algorithms are gradually being replaced by ",[215,1494,1495],{},"native:"," ones, so prefer the native name where both exist.",[177,1498,1500],{"id":1499},"troubleshooting","Troubleshooting",[182,1502,1503,1509,1519,1525,1531,1540],{},[185,1504,1505,1508],{},[188,1506,1507],{},"Zero files found."," The pattern or the path is wrong, or the extension case differs. Print the folder contents.",[185,1510,1511,1514,1515,1518],{},[188,1512,1513],{},"The run stops on the first bad file."," No per-file ",[215,1516,1517],{},"try",". Wrap the call and record the failure.",[185,1520,1521,1524],{},[188,1522,1523],{},"Outputs overwrite each other."," The output name does not vary. Derive it from the input stem.",[185,1526,1527,1530],{},[188,1528,1529],{},"A re-run redoes everything."," The skip-existing check is missing or the names do not match what was written.",[185,1532,1533,1536,1537,1539],{},[188,1534,1535],{},"The log is empty until the end."," Output buffering. Pass ",[215,1538,1404],{}," or configure logging.",[185,1541,1542,1545],{},[188,1543,1544],{},"Memory grows through the run."," Layers are being kept alive in a list. Let each iteration's layer go out of scope.",[177,1547,1549],{"id":1548},"conclusion","Conclusion",[14,1551,1552,1553,1556],{},"Discover inputs with a glob rather than a list, process each file in its own function so failures are per file, and catch broadly ",[816,1554,1555],{},"inside the loop"," while recording the file name and the error. Derive output names from the input so a re-run can skip what is already done, print a counter as the run proceeds, and finish with a three-number summary and a non-zero exit when anything failed.",[177,1558,1560],{"id":1559},"frequently-asked-questions","Frequently Asked Questions",[14,1562,1563,1566],{},[188,1564,1565],{},"Should I use the toolbox batch mode instead?","\nFor a one-off over a handful of files, yes — it is faster to set up. For anything repeated or scheduled, a script wins on error handling, resumability and reporting.",[14,1568,1569,1572],{},[188,1570,1571],{},"Can I process files in parallel?","\nCarefully. Processing algorithms are not all thread-safe, and GDAL writers can conflict. Running several QGIS processes over disjoint file lists is the safer parallelism.",[14,1574,1575,1578,1579,1582,1583,202],{},[188,1576,1577],{},"How do I merge all the outputs at the end?","\nCollect the output paths and run ",[215,1580,1581],{},"native:mergevectorlayers"," over them, which handles differing field sets — see ",[21,1584,1586],{"href":1585},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fmerge-multiple-shapefiles-pyqgis\u002F","Merge Multiple Shapefiles in PyQGIS",[14,1588,1589,1592],{},[188,1590,1591],{},"What if the algorithm needs a different parameter per file?","\nRead it from a CSV keyed by file name, or derive it from the file name itself. Keeping the mapping as data rather than code makes it reviewable.",[14,1594,1595,1598],{},[188,1596,1597],{},"Should outputs go into one GeoPackage or many files?","\nOne GeoPackage per run keeps a folder tidy and preserves types better than shapefiles. Many files are easier to distribute individually.",[14,1600,1601,1604,1605,1608,1609,202],{},[188,1602,1603],{},"How do I run this without QGIS desktop?","\nAs a standalone script or with ",[215,1606,1607],{},"qgis_process"," — see ",[21,1610,1612],{"href":1611},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fuse-qgis-process-command-line-runner\u002F","Use the qgis_process Command Line Runner",[177,1614,1616],{"id":1615},"related","Related",[182,1618,1619,1624,1628,1634,1640],{},[185,1620,1621,1623],{},[21,1622,24],{"href":23}," — the guide this recipe belongs to",[185,1625,1626],{},[21,1627,201],{"href":200},[185,1629,1630],{},[21,1631,1633],{"href":1632},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fuse-memory-layers-between-algorithms-pyqgis\u002F","Use Memory Layers Between Algorithms in PyQGIS",[185,1635,1636],{},[21,1637,1639],{"href":1638},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fautomating-shapefile-to-geojson-conversion\u002F","Automating Shapefile to GeoJSON Conversion",[185,1641,1642],{},[21,1643,1059],{"href":1058},[1645,1646,1647],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":213,"searchDepth":237,"depth":237,"links":1649},[1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661],{"id":179,"depth":237,"text":180},{"id":205,"depth":237,"text":206},{"id":440,"depth":237,"text":441},{"id":611,"depth":237,"text":612},{"id":913,"depth":237,"text":914},{"id":1062,"depth":237,"text":1063},{"id":1295,"depth":237,"text":1296},{"id":1412,"depth":237,"text":1413},{"id":1499,"depth":237,"text":1500},{"id":1548,"depth":237,"text":1549},{"id":1559,"depth":237,"text":1560},{"id":1615,"depth":237,"text":1616},"Process every dataset in a directory — discover inputs reliably, run an algorithm per file, keep going when one fails, name outputs predictably, and report what happened at the end.","md",{"slug":1665,"type":1666,"breadcrumb":1667,"datePublished":1668,"dateModified":1668},"run-algorithm-over-folder-of-files-pyqgis","article","Process a Folder","2026-08-15","\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis",{"title":5,"description":1662},"spatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis\u002Findex","QpmARrG6g5wNCvbxIavMKKgubjLvZt9gfJLUrKtJbHk",1786789586018]