Export a Graphical Model to a Python Script in PyQGIS
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.
This recipe belongs to 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.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- A working model saved as a
.model3file or in the project. Models are introduced in running a graphical model from Python.
Export the model
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.
from qgis.core import QgsProcessingModelAlgorithm, QgsProcessing
model = QgsProcessingModelAlgorithm()
if not model.fromFile("/data/models/flood_exposure.model3"):
raise RuntimeError("could not read model")
model.setName("flood_exposure_script")
lines = model.asPythonCode(QgsProcessing.PythonOutputType.PythonQgsProcessingAlgorithmSubclass, 4)
code = "\n".join(lines)
with open("/data/scripts/flood_exposure.py", "w", encoding="utf-8") as fh:
fh.write(code)
print(code[:600])
Breakdown: fromFile loads the model definition. asPythonCode with the algorithm-subclass output type produces a complete script algorithm — a class with initAlgorithm, 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 QgsProcessing.PythonQgsProcessingAlgorithmSubclass.
Read the generated code
The generated script mirrors the model step by step. Knowing its shape makes it easy to edit.
# excerpt of a generated processAlgorithm
def processAlgorithm(self, parameters, context, model_feedback):
feedback = QgsProcessingMultiStepFeedback(3, model_feedback)
results = {}
outputs = {}
# Buffer
alg_params = {
'DISTANCE': parameters['buffer_distance'],
'INPUT': parameters['rivers'],
'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT
}
outputs['Buffer'] = processing.run('native:buffer', alg_params, context=context,
feedback=feedback, is_child_algorithm=True)
feedback.setCurrentStep(1)
if feedback.isCanceled():
return {}
# Clip
alg_params = {
'INPUT': parameters['buildings'],
'OVERLAY': outputs['Buffer']['OUTPUT'],
'OUTPUT': parameters['Exposed']
}
outputs['Clip'] = processing.run('native:clip', alg_params, context=context,
feedback=feedback, is_child_algorithm=True)
results['Exposed'] = outputs['Clip']['OUTPUT']
return results
Breakdown: QgsProcessingMultiStepFeedback divides the progress bar among the steps. Each child algorithm gets a parameter dictionary built from model inputs and earlier outputs, runs with 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 results. The structure matches the patterns in chaining buffer and clip, written for you.
Clean up the generated code
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.
# before: every default listed
alg_params = {'DISSOLVE': False, 'DISTANCE': parameters['buffer_distance'],
'END_CAP_STYLE': 0, 'INPUT': parameters['rivers'], 'JOIN_STYLE': 0,
'MITER_LIMIT': 2, 'SEGMENTS': 5, 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT}
# after: only what differs from defaults, with a meaningful name
buffer_params = {'INPUT': parameters['rivers'],
'DISTANCE': parameters['buffer_distance'],
'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT}
river_buffer = processing.run('native:buffer', buffer_params, context=context,
feedback=feedback, is_child_algorithm=True)['OUTPUT']
Breakdown: 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 (river_buffer) rather than the step (outputs['Buffer']) makes the data flow obvious. Keep is_child_algorithm=True 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.
Add what the model could not do
The point of exporting is usually to add logic. Common additions are early exits, conditions and loops.
from qgis.core import QgsProcessingException
source = self.parameterAsSource(parameters, 'buildings', context)
if source.featureCount() == 0:
raise QgsProcessingException("The buildings layer is empty - nothing to assess.")
exposure = {}
for distance in (50, 100, 200):
buf = processing.run('native:buffer', {
'INPUT': parameters['rivers'], 'DISTANCE': distance,
'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT},
context=context, feedback=feedback, is_child_algorithm=True)['OUTPUT']
clipped = processing.run('native:clip', {
'INPUT': parameters['buildings'], 'OVERLAY': buf,
'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT},
context=context, feedback=feedback, is_child_algorithm=True)['OUTPUT']
layer = context.getMapLayer(clipped)
exposure[distance] = layer.featureCount()
feedback.pushInfo(f"{distance} m: {exposure[distance]} buildings exposed")
Breakdown: Raising 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 context.getMapLayer turns a child result's layer id into a layer to inspect. pushInfo writes progress lines to the log. Handling processing feedback and errors covers messages and exceptions in detail.
Register the script in the toolbox
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 processing.run.
from pathlib import Path
from qgis.core import QgsApplication
scripts_dir = Path(QgsApplication.qgisSettingsDirPath()) / "processing" / "scripts"
scripts_dir.mkdir(parents=True, exist_ok=True)
(scripts_dir / "flood_exposure.py").write_text(code, encoding="utf-8")
QgsApplication.processingRegistry().providerById("script").refreshAlgorithms()
print([a.id() for a in QgsApplication.processingRegistry().providerById("script").algorithms()])
Breakdown: The profile's processing/scripts 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 script: plus the name set in the script. For distribution to other users, scripts belong in a plugin with a processing provider, as in registering a processing provider in a plugin.
Test the script against the model
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.
import processing
params = {"rivers": "/data/tests/rivers_sample.gpkg",
"buildings": "/data/tests/buildings_sample.gpkg",
"buffer_distance": 100}
from_model = processing.run("model:flood_exposure", {**params, "Exposed": "memory:"})["Exposed"]
from_script = processing.run("script:flood_exposure_script", {**params, "Exposed": "memory:"})["Exposed"]
a, b = from_model.featureCount(), from_script.featureCount()
ids_a = sorted(f["building_id"] for f in from_model.getFeatures())
ids_b = sorted(f["building_id"] for f in from_script.getFeatures())
print(f"model {a}, script {b}, identical ids: {ids_a == ids_b}")
Breakdown: 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 testing processing algorithms with pytest — protects the script against regressions as it grows beyond what the model did.
Model or script?
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.
QGIS version compatibility
Model export with asPythonCode works on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The Python output type enum moved to 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.
Troubleshooting
- The exported script has the same id as the model. Rename the model before exporting or edit
name()in the script. - Intermediate layers appear in the project.
is_child_algorithm=Truewas removed; restore it. - 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.
- Model inputs became oddly named parameters. The generated names follow the model's input names; rename them in both
initAlgorithmandprocessAlgorithm.
Conclusion
Export a model as a script algorithm with asPythonCode, 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.
Frequently Asked Questions
Can I convert a script back into a model? No. Export is one way; keep the model if you may want to edit it visually.
Does the script run faster than the model? About the same — the same algorithms run. Gains come from logic you add, such as skipping steps.
Can scripts call models?
Yes: processing.run("model:name", params).
Is the @alg decorator an alternative?
Yes, for short scripts; see writing a processing script with the @alg decorator.