[{"data":1,"prerenderedAt":2022},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fexport-graphical-model-to-python-script-pyqgis":3},{"id":4,"title":5,"body":6,"description":2011,"extension":2012,"meta":2013,"navigation":202,"path":2018,"seo":2019,"stem":2020,"__hash__":2021},"docs\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fexport-graphical-model-to-python-script-pyqgis\u002Findex.md","Export a Graphical Model to a Python Script in PyQGIS",{"type":7,"value":8,"toc":1996},"minimark",[9,13,17,26,138,143,163,167,170,367,392,396,399,471,818,839,843,846,1038,1054,1058,1061,1116,1412,1434,1438,1444,1579,1596,1600,1603,1838,1848,1852,1855,1859,1869,1873,1910,1914,1920,1924,1930,1936,1945,1959,1963,1992],[10,11,5],"h1",{"id":12},"export-a-graphical-model-to-a-python-script-in-pyqgis",[14,15,16],"p",{},"The Model Designer is a quick way to chain algorithms: drag in inputs, connect a buffer to a clip to a dissolve, set parameters, and run. Sooner or later a model needs something boxes and arrows cannot express — a loop over a folder, a condition on a feature count, a call to an external API, a retry when a service is slow. At that point the model can be exported as Python: QGIS writes a complete Processing script algorithm that reproduces the model, which you can then read, edit and extend like any other code.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms",". It exports a model to a script, explains the structure of the generated code, cleans it up, adds logic beyond the model's reach, registers the result in the toolbox, and discusses when to stay with the model.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,100,104,108,115,121,125,128,131,134],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 246","img","A graphical model exported as a Processing script algorithm whose methods declare inputs and run each step in order","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"From boxes to a script algorithm",[40,41,42],"desc",{},"A model in the Model Designer has inputs, algorithm steps and outputs connected by arrows. Exporting it as a Python script produces a QgsProcessingAlgorithm subclass: initAlgorithm declares the same inputs and outputs, and processAlgorithm calls each step with processing.run in dependency order, passing child results along. The script can then be edited and saved in the scripts folder to appear in the toolbox.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","246","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"mdExportArrow","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-family:sans-serif;font-weight:bold","#17211d","middle","Same steps, now editable code",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"24","60","230","170","#eef7f4","#0f766e","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":83,"textAnchor":74},"139","92.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","model (.model3)",[68,92,95],{"x":87,"y":93,"style":94,"fill":66,"textAnchor":74},"120.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","inputs",[68,97,99],{"x":87,"y":98,"style":94,"fill":66,"textAnchor":74},"148.78","buffer → clip",[68,101,103],{"x":87,"y":102,"style":94,"fill":66,"textAnchor":74},"176.78","→ dissolve",[68,105,107],{"x":87,"y":106,"style":94,"fill":66,"textAnchor":74},"204.78","outputs",[109,110],"line",{"x1":111,"y1":112,"x2":113,"y2":112,"stroke":66,"style":114},"254","145","296","stroke-width:1.8;marker-end:url(#mdExportArrow)",[44,116],{"x":117,"y":79,"width":118,"height":81,"rx":58,"fill":119,"stroke":120,"style":84},"300","436","#eff3ff","#2563eb",[68,122,124],{"x":123,"y":88,"style":89,"fill":120,"textAnchor":74},"518","script algorithm (.py)",[68,126,127],{"x":123,"y":93,"style":94,"fill":66,"textAnchor":74},"initAlgorithm(): parameters",[68,129,130],{"x":123,"y":98,"style":94,"fill":66,"textAnchor":74},"processAlgorithm(): processing.run × 3",[68,132,133],{"x":123,"y":102,"style":94,"fill":66,"textAnchor":74},"results passed step to step",[68,135,137],{"x":123,"y":106,"style":94,"fill":136,"textAnchor":74},"#59645f","edit, extend, version",[139,140,142],"h2",{"id":141},"prerequisites","Prerequisites",[144,145,146,150],"ul",{},[147,148,149],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[147,151,152,153,157,158,162],{},"A working model saved as a ",[154,155,156],"code",{},".model3"," file or in the project. Models are introduced in ",[21,159,161],{"href":160},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-graphical-model-from-python-pyqgis\u002F","running a graphical model from Python",".",[139,164,166],{"id":165},"export-the-model","Export the model",[14,168,169],{},"In the Model Designer, \"Export as Script Algorithm\" writes the Python and opens it in the script editor. The same export is available from Python, which is useful for converting many models at once.",[171,172,177],"pre",{"className":173,"code":174,"language":175,"meta":176,"style":176},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProcessingModelAlgorithm, QgsProcessing\n\nmodel = QgsProcessingModelAlgorithm()\nif not model.fromFile(\"\u002Fdata\u002Fmodels\u002Fflood_exposure.model3\"):\n    raise RuntimeError(\"could not read model\")\nmodel.setName(\"flood_exposure_script\")\n\nlines = model.asPythonCode(QgsProcessing.PythonOutputType.PythonQgsProcessingAlgorithmSubclass, 4)\ncode = \"\\n\".join(lines)\nwith open(\"\u002Fdata\u002Fscripts\u002Fflood_exposure.py\", \"w\", encoding=\"utf-8\") as fh:\n    fh.write(code)\nprint(code[:600])\n","python","",[154,178,179,197,204,216,235,254,265,270,286,306,346,352],{"__ignoreMap":176},[180,181,183,187,191,194],"span",{"class":109,"line":182},1,[180,184,186],{"class":185},"snl16","from",[180,188,190],{"class":189},"s95oV"," qgis.core ",[180,192,193],{"class":185},"import",[180,195,196],{"class":189}," QgsProcessingModelAlgorithm, QgsProcessing\n",[180,198,200],{"class":109,"line":199},2,[180,201,203],{"emptyLinePlaceholder":202},true,"\n",[180,205,207,210,213],{"class":109,"line":206},3,[180,208,209],{"class":189},"model ",[180,211,212],{"class":185},"=",[180,214,215],{"class":189}," QgsProcessingModelAlgorithm()\n",[180,217,219,222,225,228,232],{"class":109,"line":218},4,[180,220,221],{"class":185},"if",[180,223,224],{"class":185}," not",[180,226,227],{"class":189}," model.fromFile(",[180,229,231],{"class":230},"sU2Wk","\"\u002Fdata\u002Fmodels\u002Fflood_exposure.model3\"",[180,233,234],{"class":189},"):\n",[180,236,238,241,245,248,251],{"class":109,"line":237},5,[180,239,240],{"class":185},"    raise",[180,242,244],{"class":243},"sDLfK"," RuntimeError",[180,246,247],{"class":189},"(",[180,249,250],{"class":230},"\"could not read model\"",[180,252,253],{"class":189},")\n",[180,255,257,260,263],{"class":109,"line":256},6,[180,258,259],{"class":189},"model.setName(",[180,261,262],{"class":230},"\"flood_exposure_script\"",[180,264,253],{"class":189},[180,266,268],{"class":109,"line":267},7,[180,269,203],{"emptyLinePlaceholder":202},[180,271,273,276,278,281,284],{"class":109,"line":272},8,[180,274,275],{"class":189},"lines ",[180,277,212],{"class":185},[180,279,280],{"class":189}," model.asPythonCode(QgsProcessing.PythonOutputType.PythonQgsProcessingAlgorithmSubclass, ",[180,282,283],{"class":243},"4",[180,285,253],{"class":189},[180,287,289,292,294,297,300,303],{"class":109,"line":288},9,[180,290,291],{"class":189},"code ",[180,293,212],{"class":185},[180,295,296],{"class":230}," \"",[180,298,299],{"class":243},"\\n",[180,301,302],{"class":230},"\"",[180,304,305],{"class":189},".join(lines)\n",[180,307,309,312,315,317,320,323,326,328,332,334,337,340,343],{"class":109,"line":308},10,[180,310,311],{"class":185},"with",[180,313,314],{"class":243}," open",[180,316,247],{"class":189},[180,318,319],{"class":230},"\"\u002Fdata\u002Fscripts\u002Fflood_exposure.py\"",[180,321,322],{"class":189},", ",[180,324,325],{"class":230},"\"w\"",[180,327,322],{"class":189},[180,329,331],{"class":330},"s9osk","encoding",[180,333,212],{"class":185},[180,335,336],{"class":230},"\"utf-8\"",[180,338,339],{"class":189},") ",[180,341,342],{"class":185},"as",[180,344,345],{"class":189}," fh:\n",[180,347,349],{"class":109,"line":348},11,[180,350,351],{"class":189},"    fh.write(code)\n",[180,353,355,358,361,364],{"class":109,"line":354},12,[180,356,357],{"class":243},"print",[180,359,360],{"class":189},"(code[:",[180,362,363],{"class":243},"600",[180,365,366],{"class":189},"])\n",[14,368,369,373,374,377,378,381,382,322,385,388,389,162],{},[370,371,372],"strong",{},"Breakdown:"," ",[154,375,376],{},"fromFile"," loads the model definition. ",[154,379,380],{},"asPythonCode"," with the algorithm-subclass output type produces a complete script algorithm — a class with ",[154,383,384],{},"initAlgorithm",[154,386,387],{},"processAlgorithm",", a name and display name — indented with the given number of spaces. Changing the model's name before export gives the script its own id, so it does not clash with the model in the toolbox. On QGIS releases before 3.30 the enum is ",[154,390,391],{},"QgsProcessing.PythonQgsProcessingAlgorithmSubclass",[139,393,395],{"id":394},"read-the-generated-code","Read the generated code",[14,397,398],{},"The generated script mirrors the model step by step. Knowing its shape makes it easy to edit.",[14,400,401],{},[29,402,405,408,411,414,417,422,426,430,434,438,441,444,447,450,453,458,462,465,468],{"viewBox":403,"role":32,"ariaLabel":404,"xmlns":34},"0 0 760 240","The exported script's structure: parameter declarations in initAlgorithm, and per-step parameter dictionaries, child runs and cancellation checks in processAlgorithm",[36,406,407],{},"Anatomy of the exported script",[40,409,410],{},"initAlgorithm adds one parameter definition per model input and one per model output. processAlgorithm creates a multi-step feedback object, then for each child algorithm builds an alg_params dictionary, calls processing.run with is_child_algorithm set, stores the output in outputs and results, and checks for cancellation between steps. The final results dictionary maps output names to layers or files.",[44,412],{"x":46,"y":46,"width":47,"height":413,"fill":49},"240",[68,415,416],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"One block per model step",[44,418],{"x":78,"y":419,"width":420,"height":421,"rx":58,"fill":82,"stroke":83,"style":84},"56","222","160",[68,423,384],{"x":424,"y":425,"style":89,"fill":83,"textAnchor":74},"135","97.78",[68,427,429],{"x":424,"y":428,"style":94,"fill":66,"textAnchor":74},"125.78","addParameter(…)",[68,431,433],{"x":424,"y":432,"style":94,"fill":66,"textAnchor":74},"153.78","per input",[68,435,437],{"x":424,"y":436,"style":94,"fill":136,"textAnchor":74},"181.78","and per output",[44,439],{"x":440,"y":419,"width":420,"height":421,"rx":58,"fill":119,"stroke":120,"style":84},"269",[68,442,443],{"x":70,"y":425,"style":89,"fill":120,"textAnchor":74},"per step",[68,445,446],{"x":70,"y":428,"style":94,"fill":66,"textAnchor":74},"alg_params = {…}",[68,448,449],{"x":70,"y":432,"style":94,"fill":66,"textAnchor":74},"processing.run(…)",[68,451,452],{"x":70,"y":436,"style":94,"fill":136,"textAnchor":74},"is_child_algorithm",[44,454],{"x":455,"y":419,"width":420,"height":421,"rx":58,"fill":456,"stroke":457,"style":84},"514","#fdf2e2","#b45309",[68,459,461],{"x":460,"y":425,"style":89,"fill":457,"textAnchor":74},"625","between steps",[68,463,464],{"x":460,"y":428,"style":94,"fill":66,"textAnchor":74},"feedback.setCurrentStep",[68,466,467],{"x":460,"y":432,"style":94,"fill":66,"textAnchor":74},"isCanceled() → {}",[68,469,470],{"x":460,"y":436,"style":94,"fill":136,"textAnchor":74},"results dict",[171,472,474],{"className":173,"code":473,"language":175,"meta":176,"style":176},"# excerpt of a generated processAlgorithm\ndef processAlgorithm(self, parameters, context, model_feedback):\n    feedback = QgsProcessingMultiStepFeedback(3, model_feedback)\n    results = {}\n    outputs = {}\n\n    # Buffer\n    alg_params = {\n        'DISTANCE': parameters['buffer_distance'],\n        'INPUT': parameters['rivers'],\n        'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT\n    }\n    outputs['Buffer'] = processing.run('native:buffer', alg_params, context=context,\n                                       feedback=feedback, is_child_algorithm=True)\n\n    feedback.setCurrentStep(1)\n    if feedback.isCanceled():\n        return {}\n\n    # Clip\n    alg_params = {\n        'INPUT': parameters['buildings'],\n        'OVERLAY': outputs['Buffer']['OUTPUT'],\n        'OUTPUT': parameters['Exposed']\n    }\n    outputs['Clip'] = processing.run('native:clip', alg_params, context=context,\n                                     feedback=feedback, is_child_algorithm=True)\n    results['Exposed'] = outputs['Clip']['OUTPUT']\n    return results\n",[154,475,476,482,494,510,520,529,533,538,548,562,574,585,590,621,641,646,657,666,674,679,685,694,706,725,738,743,768,786,809],{"__ignoreMap":176},[180,477,478],{"class":109,"line":182},[180,479,481],{"class":480},"sjoCn","# excerpt of a generated processAlgorithm\n",[180,483,484,487,491],{"class":109,"line":199},[180,485,486],{"class":185},"def",[180,488,490],{"class":489},"svObZ"," processAlgorithm",[180,492,493],{"class":189},"(self, parameters, context, model_feedback):\n",[180,495,496,499,501,504,507],{"class":109,"line":206},[180,497,498],{"class":189},"    feedback ",[180,500,212],{"class":185},[180,502,503],{"class":189}," QgsProcessingMultiStepFeedback(",[180,505,506],{"class":243},"3",[180,508,509],{"class":189},", model_feedback)\n",[180,511,512,515,517],{"class":109,"line":218},[180,513,514],{"class":189},"    results ",[180,516,212],{"class":185},[180,518,519],{"class":189}," {}\n",[180,521,522,525,527],{"class":109,"line":237},[180,523,524],{"class":189},"    outputs ",[180,526,212],{"class":185},[180,528,519],{"class":189},[180,530,531],{"class":109,"line":256},[180,532,203],{"emptyLinePlaceholder":202},[180,534,535],{"class":109,"line":267},[180,536,537],{"class":480},"    # Buffer\n",[180,539,540,543,545],{"class":109,"line":272},[180,541,542],{"class":189},"    alg_params ",[180,544,212],{"class":185},[180,546,547],{"class":189}," {\n",[180,549,550,553,556,559],{"class":109,"line":288},[180,551,552],{"class":230},"        'DISTANCE'",[180,554,555],{"class":189},": parameters[",[180,557,558],{"class":230},"'buffer_distance'",[180,560,561],{"class":189},"],\n",[180,563,564,567,569,572],{"class":109,"line":308},[180,565,566],{"class":230},"        'INPUT'",[180,568,555],{"class":189},[180,570,571],{"class":230},"'rivers'",[180,573,561],{"class":189},[180,575,576,579,582],{"class":109,"line":348},[180,577,578],{"class":230},"        'OUTPUT'",[180,580,581],{"class":189},": QgsProcessing.",[180,583,584],{"class":243},"TEMPORARY_OUTPUT\n",[180,586,587],{"class":109,"line":354},[180,588,589],{"class":189},"    }\n",[180,591,593,596,599,602,604,607,610,613,616,618],{"class":109,"line":592},13,[180,594,595],{"class":189},"    outputs[",[180,597,598],{"class":230},"'Buffer'",[180,600,601],{"class":189},"] ",[180,603,212],{"class":185},[180,605,606],{"class":189}," processing.run(",[180,608,609],{"class":230},"'native:buffer'",[180,611,612],{"class":189},", alg_params, ",[180,614,615],{"class":330},"context",[180,617,212],{"class":185},[180,619,620],{"class":189},"context,\n",[180,622,624,627,629,632,634,636,639],{"class":109,"line":623},14,[180,625,626],{"class":330},"                                       feedback",[180,628,212],{"class":185},[180,630,631],{"class":189},"feedback, ",[180,633,452],{"class":330},[180,635,212],{"class":185},[180,637,638],{"class":243},"True",[180,640,253],{"class":189},[180,642,644],{"class":109,"line":643},15,[180,645,203],{"emptyLinePlaceholder":202},[180,647,649,652,655],{"class":109,"line":648},16,[180,650,651],{"class":189},"    feedback.setCurrentStep(",[180,653,654],{"class":243},"1",[180,656,253],{"class":189},[180,658,660,663],{"class":109,"line":659},17,[180,661,662],{"class":185},"    if",[180,664,665],{"class":189}," feedback.isCanceled():\n",[180,667,669,672],{"class":109,"line":668},18,[180,670,671],{"class":185},"        return",[180,673,519],{"class":189},[180,675,677],{"class":109,"line":676},19,[180,678,203],{"emptyLinePlaceholder":202},[180,680,682],{"class":109,"line":681},20,[180,683,684],{"class":480},"    # Clip\n",[180,686,688,690,692],{"class":109,"line":687},21,[180,689,542],{"class":189},[180,691,212],{"class":185},[180,693,547],{"class":189},[180,695,697,699,701,704],{"class":109,"line":696},22,[180,698,566],{"class":230},[180,700,555],{"class":189},[180,702,703],{"class":230},"'buildings'",[180,705,561],{"class":189},[180,707,709,712,715,717,720,723],{"class":109,"line":708},23,[180,710,711],{"class":230},"        'OVERLAY'",[180,713,714],{"class":189},": outputs[",[180,716,598],{"class":230},[180,718,719],{"class":189},"][",[180,721,722],{"class":230},"'OUTPUT'",[180,724,561],{"class":189},[180,726,728,730,732,735],{"class":109,"line":727},24,[180,729,578],{"class":230},[180,731,555],{"class":189},[180,733,734],{"class":230},"'Exposed'",[180,736,737],{"class":189},"]\n",[180,739,741],{"class":109,"line":740},25,[180,742,589],{"class":189},[180,744,746,748,751,753,755,757,760,762,764,766],{"class":109,"line":745},26,[180,747,595],{"class":189},[180,749,750],{"class":230},"'Clip'",[180,752,601],{"class":189},[180,754,212],{"class":185},[180,756,606],{"class":189},[180,758,759],{"class":230},"'native:clip'",[180,761,612],{"class":189},[180,763,615],{"class":330},[180,765,212],{"class":185},[180,767,620],{"class":189},[180,769,771,774,776,778,780,782,784],{"class":109,"line":770},27,[180,772,773],{"class":330},"                                     feedback",[180,775,212],{"class":185},[180,777,631],{"class":189},[180,779,452],{"class":330},[180,781,212],{"class":185},[180,783,638],{"class":243},[180,785,253],{"class":189},[180,787,789,792,794,796,798,801,803,805,807],{"class":109,"line":788},28,[180,790,791],{"class":189},"    results[",[180,793,734],{"class":230},[180,795,601],{"class":189},[180,797,212],{"class":185},[180,799,800],{"class":189}," outputs[",[180,802,750],{"class":230},[180,804,719],{"class":189},[180,806,722],{"class":230},[180,808,737],{"class":189},[180,810,812,815],{"class":109,"line":811},29,[180,813,814],{"class":185},"    return",[180,816,817],{"class":189}," results\n",[14,819,820,373,822,825,826,829,830,833,834,838],{},[370,821,372],{},[154,823,824],{},"QgsProcessingMultiStepFeedback"," divides the progress bar among the steps. Each child algorithm gets a parameter dictionary built from model inputs and earlier outputs, runs with ",[154,827,828],{},"is_child_algorithm=True"," — which lets intermediate layers stay in the processing context instead of being loaded into the project — and stores its result under the step's name. Cancellation is checked between steps. Final outputs go into ",[154,831,832],{},"results",". The structure matches the patterns in ",[21,835,837],{"href":836},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fchain-buffer-and-clip-pyqgis\u002F","chaining buffer and clip",", written for you.",[139,840,842],{"id":841},"clean-up-the-generated-code","Clean up the generated code",[14,844,845],{},"Generated code is correct but verbose: step names from the model, every default parameter spelled out, comments with model-specific ids. A short cleanup makes it maintainable.",[171,847,849],{"className":173,"code":848,"language":175,"meta":176,"style":176},"# before: every default listed\nalg_params = {'DISSOLVE': False, 'DISTANCE': parameters['buffer_distance'],\n              'END_CAP_STYLE': 0, 'INPUT': parameters['rivers'], 'JOIN_STYLE': 0,\n              'MITER_LIMIT': 2, 'SEGMENTS': 5, 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT}\n\n# after: only what differs from defaults, with a meaningful name\nbuffer_params = {'INPUT': parameters['rivers'],\n                 'DISTANCE': parameters['buffer_distance'],\n                 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT}\nriver_buffer = processing.run('native:buffer', buffer_params, context=context,\n                              feedback=feedback, is_child_algorithm=True)['OUTPUT']\n",[154,850,851,856,886,917,948,952,957,974,985,996,1016],{"__ignoreMap":176},[180,852,853],{"class":109,"line":182},[180,854,855],{"class":480},"# before: every default listed\n",[180,857,858,861,863,866,869,872,875,877,880,882,884],{"class":109,"line":199},[180,859,860],{"class":189},"alg_params ",[180,862,212],{"class":185},[180,864,865],{"class":189}," {",[180,867,868],{"class":230},"'DISSOLVE'",[180,870,871],{"class":189},": ",[180,873,874],{"class":243},"False",[180,876,322],{"class":189},[180,878,879],{"class":230},"'DISTANCE'",[180,881,555],{"class":189},[180,883,558],{"class":230},[180,885,561],{"class":189},[180,887,888,891,893,895,897,900,902,904,907,910,912,914],{"class":109,"line":206},[180,889,890],{"class":230},"              'END_CAP_STYLE'",[180,892,871],{"class":189},[180,894,46],{"class":243},[180,896,322],{"class":189},[180,898,899],{"class":230},"'INPUT'",[180,901,555],{"class":189},[180,903,571],{"class":230},[180,905,906],{"class":189},"], ",[180,908,909],{"class":230},"'JOIN_STYLE'",[180,911,871],{"class":189},[180,913,46],{"class":243},[180,915,916],{"class":189},",\n",[180,918,919,922,924,927,929,932,934,936,938,940,942,945],{"class":109,"line":218},[180,920,921],{"class":230},"              'MITER_LIMIT'",[180,923,871],{"class":189},[180,925,926],{"class":243},"2",[180,928,322],{"class":189},[180,930,931],{"class":230},"'SEGMENTS'",[180,933,871],{"class":189},[180,935,59],{"class":243},[180,937,322],{"class":189},[180,939,722],{"class":230},[180,941,581],{"class":189},[180,943,944],{"class":243},"TEMPORARY_OUTPUT",[180,946,947],{"class":189},"}\n",[180,949,950],{"class":109,"line":237},[180,951,203],{"emptyLinePlaceholder":202},[180,953,954],{"class":109,"line":256},[180,955,956],{"class":480},"# after: only what differs from defaults, with a meaningful name\n",[180,958,959,962,964,966,968,970,972],{"class":109,"line":267},[180,960,961],{"class":189},"buffer_params ",[180,963,212],{"class":185},[180,965,865],{"class":189},[180,967,899],{"class":230},[180,969,555],{"class":189},[180,971,571],{"class":230},[180,973,561],{"class":189},[180,975,976,979,981,983],{"class":109,"line":272},[180,977,978],{"class":230},"                 'DISTANCE'",[180,980,555],{"class":189},[180,982,558],{"class":230},[180,984,561],{"class":189},[180,986,987,990,992,994],{"class":109,"line":288},[180,988,989],{"class":230},"                 'OUTPUT'",[180,991,581],{"class":189},[180,993,944],{"class":243},[180,995,947],{"class":189},[180,997,998,1001,1003,1005,1007,1010,1012,1014],{"class":109,"line":308},[180,999,1000],{"class":189},"river_buffer ",[180,1002,212],{"class":185},[180,1004,606],{"class":189},[180,1006,609],{"class":230},[180,1008,1009],{"class":189},", buffer_params, ",[180,1011,615],{"class":330},[180,1013,212],{"class":185},[180,1015,620],{"class":189},[180,1017,1018,1021,1023,1025,1027,1029,1031,1034,1036],{"class":109,"line":348},[180,1019,1020],{"class":330},"                              feedback",[180,1022,212],{"class":185},[180,1024,631],{"class":189},[180,1026,452],{"class":330},[180,1028,212],{"class":185},[180,1030,638],{"class":243},[180,1032,1033],{"class":189},")[",[180,1035,722],{"class":230},[180,1037,737],{"class":189},[14,1039,1040,1042,1043,1046,1047,1050,1051,1053],{},[370,1041,372],{}," Removing parameters that equal the algorithm's defaults shortens each step to what actually matters, which makes the next reader's job — often you, months later — much easier. Naming results after what they contain (",[154,1044,1045],{},"river_buffer",") rather than the step (",[154,1048,1049],{},"outputs['Buffer']",") makes the data flow obvious. Keep ",[154,1052,828],{}," and the feedback and context arguments: they are what make the script behave correctly inside the Processing framework, including in batch mode and when called from other models.",[139,1055,1057],{"id":1056},"add-what-the-model-could-not-do","Add what the model could not do",[14,1059,1060],{},"The point of exporting is usually to add logic. Common additions are early exits, conditions and loops.",[14,1062,1063],{},[29,1064,1067,1070,1073,1075,1078,1082,1086,1090,1094,1096,1099,1102,1105,1107,1110,1113],{"viewBox":1065,"role":32,"ariaLabel":1066,"xmlns":34},"0 0 760 222","Three additions to an exported model: an early exit on empty input, a conditional branch, and a loop over values",[36,1068,1069],{},"Logic beyond the model",[40,1071,1072],{},"Three typical additions after export. An early exit stops with a clear message when an input layer is empty. A condition chooses a different algorithm path depending on a feature count or a parameter value. A loop repeats a step for each value in a list, such as several buffer distances, collecting the results. Each is a few lines of Python and impossible or awkward in the Model Designer.",[44,1074],{"x":46,"y":46,"width":47,"height":420,"fill":49},[68,1076,1077],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"What boxes and arrows cannot say",[44,1079],{"x":78,"y":419,"width":420,"height":1080,"rx":58,"fill":456,"stroke":1081,"style":84},"150","#b91c1c",[68,1083,1085],{"x":424,"y":1084,"style":89,"fill":1081,"textAnchor":74},"106.78","early exit",[68,1087,1089],{"x":424,"y":1088,"style":94,"fill":66,"textAnchor":74},"134.78","empty input?",[68,1091,1093],{"x":424,"y":1092,"style":94,"fill":136,"textAnchor":74},"162.78","raise a clear error",[44,1095],{"x":440,"y":419,"width":420,"height":1080,"rx":58,"fill":456,"stroke":457,"style":84},[68,1097,1098],{"x":70,"y":1084,"style":89,"fill":457,"textAnchor":74},"condition",[68,1100,1101],{"x":70,"y":1088,"style":94,"fill":66,"textAnchor":74},"count > N?",[68,1103,1104],{"x":70,"y":1092,"style":94,"fill":136,"textAnchor":74},"choose a path",[44,1106],{"x":455,"y":419,"width":420,"height":1080,"rx":58,"fill":119,"stroke":120,"style":84},[68,1108,1109],{"x":460,"y":1084,"style":89,"fill":120,"textAnchor":74},"loop",[68,1111,1112],{"x":460,"y":1088,"style":94,"fill":66,"textAnchor":74},"for each distance",[68,1114,1115],{"x":460,"y":1092,"style":94,"fill":136,"textAnchor":74},"collect results",[171,1117,1119],{"className":173,"code":1118,"language":175,"meta":176,"style":176},"from qgis.core import QgsProcessingException\n\nsource = self.parameterAsSource(parameters, 'buildings', context)\nif source.featureCount() == 0:\n    raise QgsProcessingException(\"The buildings layer is empty - nothing to assess.\")\n\nexposure = {}\nfor distance in (50, 100, 200):\n    buf = processing.run('native:buffer', {\n        'INPUT': parameters['rivers'], 'DISTANCE': distance,\n        'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT},\n        context=context, feedback=feedback, is_child_algorithm=True)['OUTPUT']\n    clipped = processing.run('native:clip', {\n        'INPUT': parameters['buildings'], 'OVERLAY': buf,\n        'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT},\n        context=context, feedback=feedback, is_child_algorithm=True)['OUTPUT']\n    layer = context.getMapLayer(clipped)\n    exposure[distance] = layer.featureCount()\n    feedback.pushInfo(f\"{distance} m: {exposure[distance]} buildings exposed\")\n",[154,1120,1121,1132,1136,1154,1170,1182,1186,1195,1224,1238,1253,1264,1293,1306,1322,1332,1358,1368,1378],{"__ignoreMap":176},[180,1122,1123,1125,1127,1129],{"class":109,"line":182},[180,1124,186],{"class":185},[180,1126,190],{"class":189},[180,1128,193],{"class":185},[180,1130,1131],{"class":189}," QgsProcessingException\n",[180,1133,1134],{"class":109,"line":199},[180,1135,203],{"emptyLinePlaceholder":202},[180,1137,1138,1141,1143,1146,1149,1151],{"class":109,"line":206},[180,1139,1140],{"class":189},"source ",[180,1142,212],{"class":185},[180,1144,1145],{"class":243}," self",[180,1147,1148],{"class":189},".parameterAsSource(parameters, ",[180,1150,703],{"class":230},[180,1152,1153],{"class":189},", context)\n",[180,1155,1156,1158,1161,1164,1167],{"class":109,"line":218},[180,1157,221],{"class":185},[180,1159,1160],{"class":189}," source.featureCount() ",[180,1162,1163],{"class":185},"==",[180,1165,1166],{"class":243}," 0",[180,1168,1169],{"class":189},":\n",[180,1171,1172,1174,1177,1180],{"class":109,"line":237},[180,1173,240],{"class":185},[180,1175,1176],{"class":189}," QgsProcessingException(",[180,1178,1179],{"class":230},"\"The buildings layer is empty - nothing to assess.\"",[180,1181,253],{"class":189},[180,1183,1184],{"class":109,"line":256},[180,1185,203],{"emptyLinePlaceholder":202},[180,1187,1188,1191,1193],{"class":109,"line":267},[180,1189,1190],{"class":189},"exposure ",[180,1192,212],{"class":185},[180,1194,519],{"class":189},[180,1196,1197,1200,1203,1206,1209,1212,1214,1217,1219,1222],{"class":109,"line":272},[180,1198,1199],{"class":185},"for",[180,1201,1202],{"class":189}," distance ",[180,1204,1205],{"class":185},"in",[180,1207,1208],{"class":189}," (",[180,1210,1211],{"class":243},"50",[180,1213,322],{"class":189},[180,1215,1216],{"class":243},"100",[180,1218,322],{"class":189},[180,1220,1221],{"class":243},"200",[180,1223,234],{"class":189},[180,1225,1226,1229,1231,1233,1235],{"class":109,"line":288},[180,1227,1228],{"class":189},"    buf ",[180,1230,212],{"class":185},[180,1232,606],{"class":189},[180,1234,609],{"class":230},[180,1236,1237],{"class":189},", {\n",[180,1239,1240,1242,1244,1246,1248,1250],{"class":109,"line":308},[180,1241,566],{"class":230},[180,1243,555],{"class":189},[180,1245,571],{"class":230},[180,1247,906],{"class":189},[180,1249,879],{"class":230},[180,1251,1252],{"class":189},": distance,\n",[180,1254,1255,1257,1259,1261],{"class":109,"line":348},[180,1256,578],{"class":230},[180,1258,581],{"class":189},[180,1260,944],{"class":243},[180,1262,1263],{"class":189},"},\n",[180,1265,1266,1269,1271,1274,1277,1279,1281,1283,1285,1287,1289,1291],{"class":109,"line":354},[180,1267,1268],{"class":330},"        context",[180,1270,212],{"class":185},[180,1272,1273],{"class":189},"context, ",[180,1275,1276],{"class":330},"feedback",[180,1278,212],{"class":185},[180,1280,631],{"class":189},[180,1282,452],{"class":330},[180,1284,212],{"class":185},[180,1286,638],{"class":243},[180,1288,1033],{"class":189},[180,1290,722],{"class":230},[180,1292,737],{"class":189},[180,1294,1295,1298,1300,1302,1304],{"class":109,"line":592},[180,1296,1297],{"class":189},"    clipped ",[180,1299,212],{"class":185},[180,1301,606],{"class":189},[180,1303,759],{"class":230},[180,1305,1237],{"class":189},[180,1307,1308,1310,1312,1314,1316,1319],{"class":109,"line":623},[180,1309,566],{"class":230},[180,1311,555],{"class":189},[180,1313,703],{"class":230},[180,1315,906],{"class":189},[180,1317,1318],{"class":230},"'OVERLAY'",[180,1320,1321],{"class":189},": buf,\n",[180,1323,1324,1326,1328,1330],{"class":109,"line":643},[180,1325,578],{"class":230},[180,1327,581],{"class":189},[180,1329,944],{"class":243},[180,1331,1263],{"class":189},[180,1333,1334,1336,1338,1340,1342,1344,1346,1348,1350,1352,1354,1356],{"class":109,"line":648},[180,1335,1268],{"class":330},[180,1337,212],{"class":185},[180,1339,1273],{"class":189},[180,1341,1276],{"class":330},[180,1343,212],{"class":185},[180,1345,631],{"class":189},[180,1347,452],{"class":330},[180,1349,212],{"class":185},[180,1351,638],{"class":243},[180,1353,1033],{"class":189},[180,1355,722],{"class":230},[180,1357,737],{"class":189},[180,1359,1360,1363,1365],{"class":109,"line":659},[180,1361,1362],{"class":189},"    layer ",[180,1364,212],{"class":185},[180,1366,1367],{"class":189}," context.getMapLayer(clipped)\n",[180,1369,1370,1373,1375],{"class":109,"line":668},[180,1371,1372],{"class":189},"    exposure[distance] ",[180,1374,212],{"class":185},[180,1376,1377],{"class":189}," layer.featureCount()\n",[180,1379,1380,1383,1386,1388,1391,1394,1397,1400,1402,1405,1407,1410],{"class":109,"line":676},[180,1381,1382],{"class":189},"    feedback.pushInfo(",[180,1384,1385],{"class":185},"f",[180,1387,302],{"class":230},[180,1389,1390],{"class":243},"{",[180,1392,1393],{"class":189},"distance",[180,1395,1396],{"class":243},"}",[180,1398,1399],{"class":230}," m: ",[180,1401,1390],{"class":243},[180,1403,1404],{"class":189},"exposure[distance]",[180,1406,1396],{"class":243},[180,1408,1409],{"class":230}," buildings exposed\"",[180,1411,253],{"class":189},[14,1413,1414,1416,1417,1420,1421,1424,1425,1428,1429,1433],{},[370,1415,372],{}," Raising ",[154,1418,1419],{},"QgsProcessingException"," stops the algorithm with a message shown in the Processing dialog and log, far clearer than an empty output. The loop runs buffer and clip for several distances — something a model can only do by duplicating the steps — and ",[154,1422,1423],{},"context.getMapLayer"," turns a child result's layer id into a layer to inspect. ",[154,1426,1427],{},"pushInfo"," writes progress lines to the log. ",[21,1430,1432],{"href":1431},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fhandle-processing-feedback-and-errors-pyqgis\u002F","Handling processing feedback and errors"," covers messages and exceptions in detail.",[139,1435,1437],{"id":1436},"register-the-script-in-the-toolbox","Register the script in the toolbox",[14,1439,1440,1441,162],{},"Saved in the Processing scripts folder, the script appears in the toolbox under Scripts, usable from the dialog, in batch mode, in other models and from ",[154,1442,1443],{},"processing.run",[171,1445,1447],{"className":173,"code":1446,"language":175,"meta":176,"style":176},"from pathlib import Path\nfrom qgis.core import QgsApplication\n\nscripts_dir = Path(QgsApplication.qgisSettingsDirPath()) \u002F \"processing\" \u002F \"scripts\"\nscripts_dir.mkdir(parents=True, exist_ok=True)\n(scripts_dir \u002F \"flood_exposure.py\").write_text(code, encoding=\"utf-8\")\n\nQgsApplication.processingRegistry().providerById(\"script\").refreshAlgorithms()\nprint([a.id() for a in QgsApplication.processingRegistry().providerById(\"script\").algorithms()])\n",[154,1448,1449,1461,1472,1476,1498,1521,1542,1546,1557],{"__ignoreMap":176},[180,1450,1451,1453,1456,1458],{"class":109,"line":182},[180,1452,186],{"class":185},[180,1454,1455],{"class":189}," pathlib ",[180,1457,193],{"class":185},[180,1459,1460],{"class":189}," Path\n",[180,1462,1463,1465,1467,1469],{"class":109,"line":199},[180,1464,186],{"class":185},[180,1466,190],{"class":189},[180,1468,193],{"class":185},[180,1470,1471],{"class":189}," QgsApplication\n",[180,1473,1474],{"class":109,"line":206},[180,1475,203],{"emptyLinePlaceholder":202},[180,1477,1478,1481,1483,1486,1489,1492,1495],{"class":109,"line":218},[180,1479,1480],{"class":189},"scripts_dir ",[180,1482,212],{"class":185},[180,1484,1485],{"class":189}," Path(QgsApplication.qgisSettingsDirPath()) ",[180,1487,1488],{"class":185},"\u002F",[180,1490,1491],{"class":230}," \"processing\"",[180,1493,1494],{"class":185}," \u002F",[180,1496,1497],{"class":230}," \"scripts\"\n",[180,1499,1500,1503,1506,1508,1510,1512,1515,1517,1519],{"class":109,"line":237},[180,1501,1502],{"class":189},"scripts_dir.mkdir(",[180,1504,1505],{"class":330},"parents",[180,1507,212],{"class":185},[180,1509,638],{"class":243},[180,1511,322],{"class":189},[180,1513,1514],{"class":330},"exist_ok",[180,1516,212],{"class":185},[180,1518,638],{"class":243},[180,1520,253],{"class":189},[180,1522,1523,1526,1528,1531,1534,1536,1538,1540],{"class":109,"line":256},[180,1524,1525],{"class":189},"(scripts_dir ",[180,1527,1488],{"class":185},[180,1529,1530],{"class":230}," \"flood_exposure.py\"",[180,1532,1533],{"class":189},").write_text(code, ",[180,1535,331],{"class":330},[180,1537,212],{"class":185},[180,1539,336],{"class":230},[180,1541,253],{"class":189},[180,1543,1544],{"class":109,"line":267},[180,1545,203],{"emptyLinePlaceholder":202},[180,1547,1548,1551,1554],{"class":109,"line":272},[180,1549,1550],{"class":189},"QgsApplication.processingRegistry().providerById(",[180,1552,1553],{"class":230},"\"script\"",[180,1555,1556],{"class":189},").refreshAlgorithms()\n",[180,1558,1559,1561,1564,1566,1569,1571,1574,1576],{"class":109,"line":288},[180,1560,357],{"class":243},[180,1562,1563],{"class":189},"([a.id() ",[180,1565,1199],{"class":185},[180,1567,1568],{"class":189}," a ",[180,1570,1205],{"class":185},[180,1572,1573],{"class":189}," QgsApplication.processingRegistry().providerById(",[180,1575,1553],{"class":230},[180,1577,1578],{"class":189},").algorithms()])\n",[14,1580,1581,1583,1584,1587,1588,1591,1592,162],{},[370,1582,372],{}," The profile's ",[154,1585,1586],{},"processing\u002Fscripts"," folder is where QGIS looks for script algorithms by default; refreshing the script provider picks up the new file without restarting. The algorithm id becomes ",[154,1589,1590],{},"script:"," plus the name set in the script. For distribution to other users, scripts belong in a plugin with a processing provider, as in ",[21,1593,1595],{"href":1594},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fregister-processing-provider-in-plugin\u002F","registering a processing provider in a plugin",[139,1597,1599],{"id":1598},"test-the-script-against-the-model","Test the script against the model",[14,1601,1602],{},"Before retiring the model, prove the script does the same thing. Running both on the same small inputs and comparing outputs catches anything lost in export or cleanup.",[171,1604,1606],{"className":173,"code":1605,"language":175,"meta":176,"style":176},"import processing\n\nparams = {\"rivers\": \"\u002Fdata\u002Ftests\u002Frivers_sample.gpkg\",\n          \"buildings\": \"\u002Fdata\u002Ftests\u002Fbuildings_sample.gpkg\",\n          \"buffer_distance\": 100}\nfrom_model = processing.run(\"model:flood_exposure\", {**params, \"Exposed\": \"memory:\"})[\"Exposed\"]\nfrom_script = processing.run(\"script:flood_exposure_script\", {**params, \"Exposed\": \"memory:\"})[\"Exposed\"]\n\na, b = from_model.featureCount(), from_script.featureCount()\nids_a = sorted(f[\"building_id\"] for f in from_model.getFeatures())\nids_b = sorted(f[\"building_id\"] for f in from_script.getFeatures())\nprint(f\"model {a}, script {b}, identical ids: {ids_a == ids_b}\")\n",[154,1607,1608,1615,1619,1638,1650,1661,1697,1727,1731,1741,1769,1793],{"__ignoreMap":176},[180,1609,1610,1612],{"class":109,"line":182},[180,1611,193],{"class":185},[180,1613,1614],{"class":189}," processing\n",[180,1616,1617],{"class":109,"line":199},[180,1618,203],{"emptyLinePlaceholder":202},[180,1620,1621,1624,1626,1628,1631,1633,1636],{"class":109,"line":206},[180,1622,1623],{"class":189},"params ",[180,1625,212],{"class":185},[180,1627,865],{"class":189},[180,1629,1630],{"class":230},"\"rivers\"",[180,1632,871],{"class":189},[180,1634,1635],{"class":230},"\"\u002Fdata\u002Ftests\u002Frivers_sample.gpkg\"",[180,1637,916],{"class":189},[180,1639,1640,1643,1645,1648],{"class":109,"line":218},[180,1641,1642],{"class":230},"          \"buildings\"",[180,1644,871],{"class":189},[180,1646,1647],{"class":230},"\"\u002Fdata\u002Ftests\u002Fbuildings_sample.gpkg\"",[180,1649,916],{"class":189},[180,1651,1652,1655,1657,1659],{"class":109,"line":237},[180,1653,1654],{"class":230},"          \"buffer_distance\"",[180,1656,871],{"class":189},[180,1658,1216],{"class":243},[180,1660,947],{"class":189},[180,1662,1663,1666,1668,1670,1673,1676,1679,1682,1685,1687,1690,1693,1695],{"class":109,"line":256},[180,1664,1665],{"class":189},"from_model ",[180,1667,212],{"class":185},[180,1669,606],{"class":189},[180,1671,1672],{"class":230},"\"model:flood_exposure\"",[180,1674,1675],{"class":189},", {",[180,1677,1678],{"class":185},"**",[180,1680,1681],{"class":189},"params, ",[180,1683,1684],{"class":230},"\"Exposed\"",[180,1686,871],{"class":189},[180,1688,1689],{"class":230},"\"memory:\"",[180,1691,1692],{"class":189},"})[",[180,1694,1684],{"class":230},[180,1696,737],{"class":189},[180,1698,1699,1702,1704,1706,1709,1711,1713,1715,1717,1719,1721,1723,1725],{"class":109,"line":267},[180,1700,1701],{"class":189},"from_script ",[180,1703,212],{"class":185},[180,1705,606],{"class":189},[180,1707,1708],{"class":230},"\"script:flood_exposure_script\"",[180,1710,1675],{"class":189},[180,1712,1678],{"class":185},[180,1714,1681],{"class":189},[180,1716,1684],{"class":230},[180,1718,871],{"class":189},[180,1720,1689],{"class":230},[180,1722,1692],{"class":189},[180,1724,1684],{"class":230},[180,1726,737],{"class":189},[180,1728,1729],{"class":109,"line":272},[180,1730,203],{"emptyLinePlaceholder":202},[180,1732,1733,1736,1738],{"class":109,"line":288},[180,1734,1735],{"class":189},"a, b ",[180,1737,212],{"class":185},[180,1739,1740],{"class":189}," from_model.featureCount(), from_script.featureCount()\n",[180,1742,1743,1746,1748,1751,1754,1757,1759,1761,1764,1766],{"class":109,"line":308},[180,1744,1745],{"class":189},"ids_a ",[180,1747,212],{"class":185},[180,1749,1750],{"class":243}," sorted",[180,1752,1753],{"class":189},"(f[",[180,1755,1756],{"class":230},"\"building_id\"",[180,1758,601],{"class":189},[180,1760,1199],{"class":185},[180,1762,1763],{"class":189}," f ",[180,1765,1205],{"class":185},[180,1767,1768],{"class":189}," from_model.getFeatures())\n",[180,1770,1771,1774,1776,1778,1780,1782,1784,1786,1788,1790],{"class":109,"line":348},[180,1772,1773],{"class":189},"ids_b ",[180,1775,212],{"class":185},[180,1777,1750],{"class":243},[180,1779,1753],{"class":189},[180,1781,1756],{"class":230},[180,1783,601],{"class":189},[180,1785,1199],{"class":185},[180,1787,1763],{"class":189},[180,1789,1205],{"class":185},[180,1791,1792],{"class":189}," from_script.getFeatures())\n",[180,1794,1795,1797,1799,1801,1804,1806,1808,1810,1813,1815,1818,1820,1823,1825,1827,1829,1832,1834,1836],{"class":109,"line":354},[180,1796,357],{"class":243},[180,1798,247],{"class":189},[180,1800,1385],{"class":185},[180,1802,1803],{"class":230},"\"model ",[180,1805,1390],{"class":243},[180,1807,21],{"class":189},[180,1809,1396],{"class":243},[180,1811,1812],{"class":230},", script ",[180,1814,1390],{"class":243},[180,1816,1817],{"class":189},"b",[180,1819,1396],{"class":243},[180,1821,1822],{"class":230},", identical ids: ",[180,1824,1390],{"class":243},[180,1826,1745],{"class":189},[180,1828,1163],{"class":185},[180,1830,1831],{"class":189}," ids_b",[180,1833,1396],{"class":243},[180,1835,302],{"class":230},[180,1837,253],{"class":189},[14,1839,1840,1842,1843,1847],{},[370,1841,372],{}," Running both with identical parameters on a small sample makes the comparison fast; comparing feature counts and a sorted list of business keys is a strong check without having to compare geometries. Keeping a sample dataset and this comparison as an automated test — see ",[21,1844,1846],{"href":1845},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Ftest-processing-algorithms-with-pytest-qgis\u002F","testing processing algorithms with pytest"," — protects the script against regressions as it grows beyond what the model did.",[139,1849,1851],{"id":1850},"model-or-script","Model or script?",[14,1853,1854],{},"Models remain the better choice when the workflow is a straightforward chain, when colleagues who do not write Python need to understand and modify it, and when the visual diagram is itself documentation. Scripts win when logic is needed, when the workflow is versioned and reviewed like code, and when it must be tested automatically. A useful middle path is to keep the model as the readable specification and export to a script only once, at the point where code becomes necessary — then maintain the script and retire the model, rather than trying to keep both in sync.",[139,1856,1858],{"id":1857},"qgis-version-compatibility","QGIS version compatibility",[14,1860,1861,1862,1864,1865,1868],{},"Model export with ",[154,1863,380],{}," works on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The Python output type enum moved to ",[154,1866,1867],{},"QgsProcessing.PythonOutputType"," in 3.30; QGIS 4 uses only the scoped form. Generated code targets the running QGIS version; a script exported from a newer release may use APIs unavailable in older ones.",[139,1870,1872],{"id":1871},"troubleshooting","Troubleshooting",[144,1874,1875,1885,1893,1899],{},[147,1876,1877,1880,1881,1884],{},[370,1878,1879],{},"The exported script has the same id as the model."," Rename the model before exporting or edit ",[154,1882,1883],{},"name()"," in the script.",[147,1886,1887,373,1890,1892],{},[370,1888,1889],{},"Intermediate layers appear in the project.",[154,1891,828],{}," was removed; restore it.",[147,1894,1895,1898],{},[370,1896,1897],{},"The script is missing from the toolbox."," It is not in a scripts folder, or the provider was not refreshed; check the log for syntax errors.",[147,1900,1901,1904,1905,1907,1908,162],{},[370,1902,1903],{},"Model inputs became oddly named parameters."," The generated names follow the model's input names; rename them in both ",[154,1906,384],{}," and ",[154,1909,387],{},[139,1911,1913],{"id":1912},"conclusion","Conclusion",[14,1915,1916,1917,1919],{},"Export a model as a script algorithm with ",[154,1918,380],{},", read its per-step structure, trim default parameters and name results after their content, add early exits, conditions and loops the model could not express, save it to the scripts folder or a plugin provider, and choose models for simple shared chains and scripts once logic or testing is needed.",[139,1921,1923],{"id":1922},"frequently-asked-questions","Frequently Asked Questions",[14,1925,1926,1929],{},[370,1927,1928],{},"Can I convert a script back into a model?","\nNo. Export is one way; keep the model if you may want to edit it visually.",[14,1931,1932,1935],{},[370,1933,1934],{},"Does the script run faster than the model?","\nAbout the same — the same algorithms run. Gains come from logic you add, such as skipping steps.",[14,1937,1938,1941,1942,162],{},[370,1939,1940],{},"Can scripts call models?","\nYes: ",[154,1943,1944],{},"processing.run(\"model:name\", params)",[14,1946,1947,1954,1955,162],{},[370,1948,1949,1950,1953],{},"Is the ",[154,1951,1952],{},"@alg"," decorator an alternative?","\nYes, for short scripts; see ",[21,1956,1958],{"href":1957},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-processing-script-with-alg-decorator\u002F","writing a processing script with the @alg decorator",[139,1960,1962],{"id":1961},"related","Related",[144,1964,1965,1970,1975,1981,1986],{},[147,1966,1967,1969],{},[21,1968,24],{"href":23}," — the guide this recipe belongs to",[147,1971,1972],{},[21,1973,1974],{"href":160},"Run a Graphical Model from Python in PyQGIS",[147,1976,1977],{},[21,1978,1980],{"href":1979},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-custom-processing-algorithm-pyqgis\u002F","Write a Custom Processing Algorithm in PyQGIS",[147,1982,1983],{},[21,1984,1985],{"href":1431},"Handle Processing Feedback and Errors in PyQGIS",[147,1987,1988],{},[21,1989,1991],{"href":1990},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-grass-and-saga-algorithms-pyqgis\u002F","Run GRASS and SAGA Algorithms from PyQGIS",[1993,1994,1995],"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":176,"searchDepth":199,"depth":199,"links":1997},[1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010],{"id":141,"depth":199,"text":142},{"id":165,"depth":199,"text":166},{"id":394,"depth":199,"text":395},{"id":841,"depth":199,"text":842},{"id":1056,"depth":199,"text":1057},{"id":1436,"depth":199,"text":1437},{"id":1598,"depth":199,"text":1599},{"id":1850,"depth":199,"text":1851},{"id":1857,"depth":199,"text":1858},{"id":1871,"depth":199,"text":1872},{"id":1912,"depth":199,"text":1913},{"id":1922,"depth":199,"text":1923},{"id":1961,"depth":199,"text":1962},"Turn a Processing model built in the Model Designer into Python code — exporting it as a Processing script algorithm, reading and cleaning the generated code, adding logic models cannot express, and deciding when to keep the model and when to switch to code.","md",{"slug":2014,"type":2015,"breadcrumb":2016,"datePublished":2017,"dateModified":2017},"export-graphical-model-to-python-script-pyqgis","article","Export a Model to a Python Script","2026-10-02","\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fexport-graphical-model-to-python-script-pyqgis",{"title":5,"description":2011},"spatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fexport-graphical-model-to-python-script-pyqgis\u002Findex","DnpB6R70smg-Q1T2EuLiEAVof5VkwD9RESCaVCES8EU",1790966260894]