GIS & Geospatial Data (QGIS, GeoPandas, PostGIS) Module 4 – QGIS desktop
Automating QGIS with PyQGIS
Run PyQGIS in the console and as a headless script, request only the features you need, edit safely, call Processing and script a monthly report.
What you will learn
- Run PyQGIS in the Python console and as a standalone script that starts QGIS without a window
- Load layers, request only the features you need, and edit them in a session that can roll back
- Run Processing algorithms from Python and export a report's layouts without the QGIS window
- Move a layer between PyQGIS and GeoPandas, and write scheduled code that runs on QGIS 4's Qt6
Before you start
What you’ll learn · Before you start
What you will learn
- Run PyQGIS in the Python console and as a standalone script that starts QGIS without a window
- Load layers, request only the features you need, and edit them in a session that can roll back
- Run Processing algorithms from Python and export a report's layouts without the QGIS window
- Move a layer between PyQGIS and GeoPandas, and write scheduled code that runs on QGIS 4's Qt6
Before you start
On this page
Every month the town’s report is the same work on new data: count the households near each clinic, write a GeoPackage and a CSV, and print a page for each of the 12 wards. Done in dialogs, every click is repeated; this lesson’s script writes all 14 files and checks them before it ends.
PyQGIS: QGIS's Python interface: the classes behind the QGIS window, such as layers, features, Processing algorithms and print layouts, used from Python in the console, in plugins and in standalone scripts. is QGIS’s Python interface: the layers, features, algorithms and layouts of the QGIS window, driven from code. You will run it in the console and as a script that starts QGIS itself, read and edit layers, call Processing, and build the report, all on QGIS 4.2.3.
PyQGIS runs inside QGIS, or starts QGIS itself
Plugins ▸ Python Console opens a console inside the running QGIS: qgis.core and qgis.utils are already imported,
and iface reaches the map, the menus and the toolbars (cookbook).
A standalone script: A Python program that uses PyQGIS without the QGIS window around it: it starts QGIS itself with QgsApplication and initQgis(), does its work, and ends with exitQgis(). Scheduled jobs and servers run them. has no QGIS around it, so it starts one, without a window:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The environment. A server has no screen, so the script sets QT_QPA_PLATFORM to offscreen before QGIS starts. If qgis.core will not import, Python needs QGIS's own python folder on its path.
- The prefix path. setPrefixPath tells QGIS where it is installed, so that it finds its data providers, its resources and its Processing plugins.
- The application. QgsApplication([], False) makes the QGIS application with no command-line arguments, and False means no window: the script needs no graphical interface.
- initQgis. initQgis() loads the data providers and the layer registry. Only now can the script open a GeoPackage or run an algorithm.
- The work. The script loads layers, asks for features, edits, runs Processing and exports layouts, with the same classes the QGIS window uses.
- exitQgis. exitQgis() removes the data providers and the layer registry from memory before Python exits. In the Python console none of these steps is needed, because QGIS is already running.
This program starts and stops QGIS itself, with no helper, and in between writes and reads the town’s GeoPackage:
"""A standalone PyQGIS script: QGIS started without a window, a GeoPackage read, and QGIS closed again."""
import os
import sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") # draw without a screen, as on a server
os.environ.setdefault("QT_LOGGING_RULES", "qt.*=false") # keep Qt's own notes out of the output
try:
from qgis.core import QgsApplication
except ImportError: # a conda-forge QGIS keeps its Python package in share/qgis/python
sys.path.insert(0, os.path.join(sys.prefix, "share", "qgis", "python"))
from qgis.core import QgsApplication
QgsApplication.setPrefixPath(os.environ.get("QGIS_PREFIX_PATH", sys.prefix), True) # where QGIS is installed
app = QgsApplication([], False) # no command-line arguments, and False: no window
app.initQgis() # loads the data providers and the layer registry
from qgis.core import Qgis, QgsProviderRegistry, QgsVectorLayer, QgsWkbTypes # noqa: E402
from town_files import write_town # noqa: E402
print("QGIS", Qgis.version().split("-")[0], "| data providers:", len(QgsProviderRegistry.instance().providerList()))
write_town("town.gpkg")
for name in ("wards", "households", "clinics"):
layer = QgsVectorLayer(f"town.gpkg|layername={name}", name, "ogr")
print(f" {name:10} valid {layer.isValid()}, {layer.featureCount():5,} features,",
f"{QgsWkbTypes.displayString(layer.wkbType())}, {layer.crs().authid()}")
del layer # the layer goes before QGIS does
app.exitQgis() # lets go of the data providers before Python exits
print("exitQgis: done") Output
QGIS 4.2.3 | data providers: 34 wards valid True, 12 features, Polygon, EPSG:32643 households valid True, 5,000 features, Point, EPSG:32643 clinics valid True, 8 features, Point, EPSG:32643 exitQgis: done
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 standalone.py
initQgis loaded 34 data providers, the GeoPackage’s three layers opened valid, and exitQgis ran last, releasing the
providers before Python exits, as the cookbook asks. When import qgis.core fails, Python cannot see QGIS’s own
python folder: the cookbook names share/qgis/python on Linux and the app bundle’s Contents/Resources/python on
macOS, and the script adds the conda-forge one itself. The other programs of this lesson start QGIS the same way
through a small helper, qgis_start.py.
Checkpoint
Check: The console and standalone scripts
Ask a layer for only the features you need
A loop over layer.getFeatures() reads every feature with every field and its geometry. A
feature request: A QgsFeatureRequest: what a script asks a layer for, such as the features matching an expression or inside a rectangle, a limit, only some fields, or no geometry, so that it reads no more than it needs. narrows that: an expression, a rectangle, a limit, a subset of the
fields, or no geometry at all
(vector layers). Five requests on the
town’s households:
"""Five feature requests: ask a layer for only the features, the fields and the geometry that a task needs."""
import qgis_start
from qgis.core import Qgis, QgsExpression, QgsFeatureRequest, QgsGeometry, QgsVectorLayer
from town_files import write_town
write_town("town.gpkg")
households = QgsVectorLayer("town.gpkg|layername=households", "households", "ogr")
wards = QgsVectorLayer("town.gpkg|layername=wards", "wards", "ogr")
# 1. An attribute filter: an expression, evaluated by the data provider where it can be.
w04 = next(wards.getFeatures(QgsFeatureRequest(QgsExpression("\"ward\" = 'W04'"))))
print("Ward W04 found by an expression; its box:", w04.geometry().boundingBox().toString(0))
# 2. A spatial filter: features whose box meets a rectangle, found through the GeoPackage's spatial index.
request = QgsFeatureRequest().setFilterRect(w04.geometry().boundingBox())
inside = sum(1 for _ in households.getFeatures(request))
everything = sum(1 for _ in households.getFeatures())
print(f"Households in W04's box: {inside:,}, read instead of all {everything:,}")
# 3. A rectangle stays a rectangle: an L of three wards, W03, W04 and W08, has a fourth ward, W07, inside its box.
shapes = {f["ward"]: f.geometry() for f in wards.getFeatures()}
l_shape = QgsGeometry.unaryUnion([shapes["W03"], shapes["W04"], shapes["W08"]])
box_only = QgsFeatureRequest().setFilterRect(l_shape.boundingBox())
exact = QgsFeatureRequest(box_only).setFlags(Qgis.FeatureRequestFlag.ExactIntersect) # exact, but against the box
in_l = QgsFeatureRequest().setDistanceWithin(l_shape, 0) # the index finds candidates, the L's own shape decides
counts = [sum(1 for _ in households.getFeatures(request)) for request in (box_only, exact, in_l)]
print("An L of W03, W04 and W08: {:,} households in its box, {:,} with ExactIntersect, {:,} inside the L".format(*counts))
# 4. Only some fields, and no geometry at all, when the task reads one attribute.
names = QgsFeatureRequest()
names.setSubsetOfAttributes(["household"], households.fields())
names.setFlags(Qgis.FeatureRequestFlag.NoGeometry)
listed = [f for f in households.getFeatures(names)]
print(f"NoGeometry and one field: {len(listed):,} names read, first {listed[0]['household']},",
"any geometry read:", any(f.hasGeometry() for f in listed))
both = QgsFeatureRequest(names).setFilterRect(w04.geometry().boundingBox())
in_box = [f for f in households.getFeatures(both)]
print(f"The same request inside W04's box: {len(in_box):,} names; geometry read for the box test:",
all(f.hasGeometry() for f in in_box))
# 5. A limit: the first few features, for a preview or a quick test.
preview = [f["household"] for f in households.getFeatures(QgsFeatureRequest().setLimit(3))]
print("setLimit(3):", ", ".join(preview)) Output
Ward W04 found by an expression; its box: 378000,2048000 : 380000,2050000 Households in W04's box: 1,169, read instead of all 5,000 An L of W03, W04 and W08: 2,071 households in its box, 2,071 with ExactIntersect, 1,692 inside the L NoGeometry and one field: 5,000 names read, first H0001, any geometry read: False The same request inside W04's box: 1,169 names; geometry read for the box test: True setLimit(3): H0001, H0002, H0003
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 requests.py
The rectangle of W04 read 1,169 households instead of all 5,000; W04 is a square, so its box holds exactly its
households. The box of an L made of W03, W04 and W08 also covers W07, and read 2,071. ExactIntersect changed nothing:
it tests each feature exactly against the rectangle, and a point is its own box, so the flag helps with lines and
polygons, not points. setDistanceWithin(l_shape, 0)
(API) let the index find candidates and the L’s own shape
decide: 1,692.
NoGeometry read 5,000 names and not one geometry, but the same request inside W04’s box read geometries anyway, because the box test needs them. setLimit(3) returned H0001 to H0003, the first features in the layer’s order: a quick preview, never a count.
Checkpoint
Check: Feature requests
Edits go through a session that commits or rolls back
Edits made between startEditing() and commitChanges() stay in the layer’s edit buffer until they are saved, and
rollBack() drops them all. with edit(layer): does both for you: it commits when the block ends and rolls back when
it raises an error. Writing through layer.dataProvider() instead goes to the file at once, with no undo:
"""Editing a layer in a session: changes that are kept together on success, or all thrown away on an error."""
import qgis_start
from qgis.core import QgsField, QgsVectorLayer, edit
from qgis.PyQt.QtCore import QMetaType
from town_files import write_town
write_town("town.gpkg")
wards = QgsVectorLayer("town.gpkg|layername=wards", "wards", "ogr")
def saved_areas():
"""The area_km2 values as the file holds them, read by a fresh layer."""
fresh = QgsVectorLayer("town.gpkg|layername=wards", "check", "ogr")
return [f["area_km2"] for f in fresh.getFeatures()][:3]
with edit(wards): # starts an edit session, and commits it when the block ends without an error
wards.addAttribute(QgsField("area_km2", QMetaType.Type.Double))
wards.updateFields()
column = wards.fields().indexOf("area_km2")
for ward in wards.getFeatures():
wards.changeAttributeValue(ward.id(), column, round(ward.geometry().area() / 1e6, 2))
print("Committed: the file now holds area_km2 =", saved_areas(), "...")
try:
with edit(wards):
for ward in wards.getFeatures():
wards.changeAttributeValue(ward.id(), column, 0.0)
if ward["ward"] == "W02":
raise ValueError("a check failed at W02, halfway through")
except ValueError as error:
print("The second session stopped:", error)
print("Rolled back: still editing?", wards.isEditable(), "| the file holds", saved_areas(), "...")
# The data provider writes at once: no session, no undo.
wards.dataProvider().changeAttributeValues({next(wards.getFeatures()).id(): {column: 9.99}})
print("Written straight through the data provider:", saved_areas(), "...") Output
Committed: the file now holds area_km2 = [4.0, 4.0, 4.0] ... The second session stopped: a check failed at W02, halfway through Rolled back: still editing? False | the file holds [4.0, 4.0, 4.0] ... Written straight through the data provider: [9.99, 4.0, 4.0] ...
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 edit_session.py
The first edit session: The time between starting to edit a layer and committing or rolling back. Changes wait in the layer's edit buffer, so they reach the file together, at the commit, or not at all. added area_km2, 4.0 for every 2 km ward. The second failed at W02
and nothing of it reached the file, while the provider’s write of 9.99 did, at once. Use a session whenever a script
may stop halfway. Inside a session each change can also be undone on its own: the cookbook wraps it in
beginEditCommand() and endEditCommand(), so that Undo can step back through the changes one by one.
Checkpoint
Check: Edit sessions that roll back
processing.run calls the toolbox from Python
processing.run takes an algorithm’s id and a dictionary of its parameters, the same names as the toolbox and
qgis_process, and returns a dictionary of outputs, keyed by their names
(console):
"""The toolbox's algorithms called from Python: the same ids and parameter names, and a dictionary of results back."""
import qgis_start
from qgis.core import QgsVectorLayer
from town_files import write_town
processing = qgis_start.processing()
write_town("town.gpkg")
near = processing.run("native:extractwithindistance", {
"INPUT": "town.gpkg|layername=households", "REFERENCE": "town.gpkg|layername=clinics", "DISTANCE": 1000,
"OUTPUT": "memory:"})["OUTPUT"]
print("Extract within distance returned a layer of", f"{near.featureCount():,}", "households")
result = processing.run("native:countpointsinpolygon", {
"POLYGONS": "town.gpkg|layername=wards", "POINTS": near, "FIELD": "near", "OUTPUT": "near.gpkg"})
print("Count points in polygon returned", result)
counted = QgsVectorLayer(result["OUTPUT"], "near", "ogr")
print("Per ward:", ", ".join(f"{f['ward']} {f['near']:.0f}" for f in counted.getFeatures())) Output
Extract within distance returned a layer of 2,973 households
Count points in polygon returned {'OUTPUT': 'near.gpkg'}
Per ward: W01 210, W02 304, W03 100, W04 691, W05 27, W06 320, W07 313, W08 146, W09 94, W10 0, W11 444, W12 324
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 processing_from_python.py
Extract within distance kept 2,973 households, the count that measures every distance, and Count points in polygon
wrote near.gpkg, whose path came back under OUTPUT. processing.algorithmHelp("native:extractwithindistance") lists
an algorithm’s parameters before you call it, and runAndLoadResults() adds the outputs to the open project, which
run() never does. A script can also export layouts for a scheduled report, hand a layer to GeoPandas with
as_geopandas(), and end with an exit code that tells a scheduler the truth. On QGIS 4 it is written with Qt6’s
scoped names, and plugins that run code deserve the same care.
Checkpoint
Check: Processing from Python
The monthly report in one script: 14 files, written and checked Pro: opens with a Pro, Premium or Ultimate pass
A report that runs every month must be right without anyone looking. This part builds the town’s report in one script, from the analysis to 12 PDFs, and makes it check its own numbers before it ends.
In this part:
- Fourteen files from one QGIS session
- Checks that stop a wrong report from going out
- Running it every month without a person
1 runnable example (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
From a QGIS layer to GeoPandas and back, compared column by column Pro: opens with a Pro, Premium or Ultimate pass
Some work is easier in a table than in a layer: grouping, joining with a spreadsheet, statistics. This part hands a QGIS layer to GeoPandas with QGIS 4.0’s new method, compares it with GeoPandas’ own reading of the same file, and brings the result back.
In this part:
- as_geopandas() against read_file(), measured
- Handing the result back to QGIS
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Scheduled runs that tell the truth, and code that runs on QGIS 4 Pro: opens with a Pro, Premium or Ultimate pass
A scheduler judges a job by its exit code alone. This part runs one small nightly job three ways and shows an exit code that lied, then the habits that keep a script working on QGIS 4 and safe to run.
In this part:
- One job, three exit codes
- Code that runs on QGIS 4, and plugins you can trust
1 runnable example (Python, job.py) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: check a report’s summary before it goes out
Write the check that stops a wrong summary: missing and repeated wards, impossible counts, a share that does not match its counts, and a total that does not add up. The sample tests use this lesson’s own summary:
Exercise · Easy · Python
Check a report's summary before it goes out
A scheduled report should stop instead of sending wrong numbers. Write report_problems(rows, wards, total) in summary_check.py. rows are the rows of the report's summary.csv as dictionaries with ward, households, near and share_pct, wards is the list of every ward the report must cover, and total the number of households the town has. Return a list of problems, in this order:
- for each ward in
wards, in its order:W05: missingif no row has it,W05: 2 rowsif more than one does; - for each row, in its order:
W07: near 400 is more than households 379when near is larger; otherwiseW02: share_pct 43.0, but near / households gives 43.7when share_pct differs fromround(100 * near / households, 1)(write both numbers with one decimal); - last, if the households of all rows do not add up to
total:households add up to 4,999, not 5,000, with a comma between thousands.
Return an empty list for a summary with no problem. The sample tests use the lesson's own summary.
Starter code · summary_check.py
def report_problems(rows, wards, total):
"""Everything wrong with a report's summary rows, in the order the prompt gives; [] when nothing is."""
# Replace this line: missing and repeated wards first, then each row's numbers, then the town's total.
return [] The sample tests · test_summary_check.py
from summary_check import report_problems
WARDS = [f"W{n:02d}" for n in range(1, 13)]
HOUSEHOLDS = [295, 696, 183, 1169, 204, 416, 379, 340, 126, 151, 648, 393]
NEAR = [210, 304, 100, 691, 27, 320, 313, 146, 94, 0, 444, 324] # within 1,000 m of a clinic, by distance
def summary():
"""The lesson's summary.csv, one row per ward."""
return [{"ward": w, "households": h, "near": n, "share_pct": round(100 * n / h, 1)}
for w, h, n in zip(WARDS, HOUSEHOLDS, NEAR)]
def test_the_lessons_summary():
"""finds nothing wrong with the lesson's own summary"""
assert report_problems(summary(), WARDS, 5000) == []
def test_missing_and_repeated_wards():
"""names a ward with no row and a ward with two, then the total they break"""
rows = summary()
rows[4] = dict(rows[2]) # W05's row lost, W03's written twice
assert report_problems(rows, WARDS, 5000) == ["W03: 2 rows", "W05: missing",
"households add up to 4,979, not 5,000"]
def test_numbers_that_cannot_be_right():
"""catches more households near a clinic than live in the ward, and a share that does not match its counts"""
rows = summary()
rows[6]["near"] = 400
rows[6]["share_pct"] = 105.5
rows[1]["share_pct"] = 43.0
assert report_problems(rows, WARDS, 5000) == ["W02: share_pct 43.0, but near / households gives 43.7",
"W07: near 400 is more than households 379"]
def test_a_total_that_does_not_add_up():
"""compares the households of all rows with the town's total"""
assert report_problems(summary(), WARDS, 5001) == ["households add up to 5,000, not 5,001"] A hint
Count the rows per ward first with a dictionary, seen[row["ward"]] = seen.get(row["ward"], 0) + 1, then walk wards for the missing and repeated ones. f"{value:.1f}" writes one decimal and f"{value:,}" a comma between thousands.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then test yourself on standalone scripts, feature requests, edits and Processing:
Practice that adapts to you
Practise this lesson
9 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What is the difference between PyQGIS in the Python console and in a standalone script?
In the console QGIS is already running: qgis.core is imported and iface gives access to the window, so code acts on
the open project. A standalone script runs without QGIS around it, so it starts QGIS itself, with the prefix path,
QgsApplication([], False) and initQgis(), and ends with exitQgis().
Three questions of this module’s bank start from this lesson:
-
Mid to senior
A nightly PyQGIS report has written all 12 ward PDFs every night for a month, yet the scheduler marks every run failed with exit code 139. Nobody has found anything wrong in the files. What is most likely happening, and how do you make the job's exit code trustworthy?
-
Mid to senior
A PyQGIS script counts 2 million address points per district by looping over every point and testing it against each of 300 districts. It runs for hours. How would you make it fast, and how would you show that the fast version gives the same counts?
-
Mid to senior
Your team's in-house QGIS plugin must keep working for analysts on the long-term release, QGIS 3.44, and on QGIS 4.2. What do you change in its code and its metadata, and how do you test it before you release it?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does PyQGIS in the Python console differ from PyQGIS in a standalone script?
In the console QGIS is already running: qgis.core is imported and iface reaches the window. A standalone script starts QGIS itself with QgsApplication([], False) and initQgis(), and ends with exitQgis(), which releases the providers before Python exits.
-
What does it mean when import qgis.core fails in a standalone script?
Python cannot see QGIS's own python folder: share/qgis/python on Linux, or Contents/Resources/python inside the app bundle on macOS. Add that folder to Python's path.
-
What can a QgsFeatureRequest narrow down?
What is read: features by an expression, a rectangle or a limit, and of each feature only some fields, or no geometry at all. A loop over getFeatures() without a request reads every field and geometry of every feature.
-
A rectangle request over an L-shaped area read 2,071 households, but the L held 1,692. Why?
A rectangle request tests against the box, and the box of an L made of three wards also covers a fourth. setDistanceWithin(shape, 0) lets the index find candidates and the L's own shape decide.
-
Why did the ExactIntersect flag change nothing for a rectangle request on points?
It tests each feature exactly against the rectangle instead of by its box, but a point is its own box, so the flag only matters for lines and polygons.
-
How does editing in a session differ from writing through layer.dataProvider()?
Edits between startEditing() and commitChanges() wait in the edit buffer, and rollBack() drops them all; with edit(layer): commits at the end of the block and rolls back on an error. A provider write reaches the file at once, with no undo.
-
What does processing.run take and return, and how does runAndLoadResults differ?
It takes an algorithm id and a dictionary of parameters named as in the toolbox and qgis_process, and returns a dictionary of outputs by name. runAndLoadResults() also adds the outputs to the open project, which run() never does.
Key takeaways
- PyQGIS runs inside QGIS’s console, or in a standalone script that starts QGIS without a window and ends with exitQgis.
- A feature request reads only what a task needs: a rectangle read 1,169 of 5,000 households, and an area that is not a box needs a test against its own shape.
- An edit session commits its changes together or rolls them all back; the data provider writes at once.
- processing.run takes the toolbox’s ids and parameter names and returns a dictionary of outputs.
Terms in this lesson
- edit session
- The time between starting to edit a layer and committing or rolling back. Changes wait in the layer's edit buffer, so they reach the file together, at the commit, or not at all.
- feature request
- A QgsFeatureRequest: what a script asks a layer for, such as the features matching an expression or inside a rectangle, a limit, only some fields, or no geometry, so that it reads no more than it needs.
- PyQGIS
- QGIS's Python interface: the classes behind the QGIS window, such as layers, features, Processing algorithms and print layouts, used from Python in the console, in plugins and in standalone scripts.
- standalone script
- A Python program that uses PyQGIS without the QGIS window around it: it starts QGIS itself with QgsApplication and initQgis(), does its work, and ends with exitQgis(). Scheduled jobs and servers run them.
References
- PyQGIS Developer Cookbook: introduction (the console and standalone scripts) (QGIS Project)
- PyQGIS Developer Cookbook: using vector layers (QGIS Project)
- QGIS Python API 4.2: QgsFeatureRequest (QGIS Project)
- QGIS User Guide: using processing algorithms from the console (QGIS Project)
- PyQGIS Developer Cookbook: map rendering and printing (QGIS Project)
- Changelog for QGIS 4.0 (QGIS Project)
- Plugin migration to be compatible with Qt5 and Qt6 (QGIS Project)
- Plugin repository security enhancements (QGIS Project)
- Bash Reference Manual, exit status (GNU Project (Free Software Foundation))
- subprocess, subprocess management (Popen.returncode) (Python Software Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress