Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, qgis 4.2.3, geopandas 1.2.0, pyogrio 0.13.0 and shapely 2.1.2
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: PyQGIS runs inside QGIS, or starts QGIS itself · Ask a layer for only the features you need · Edits go through a session that commits or rolls back · processing.run calls the toolbox from Python. With a pass: 3 in-depth parts and 3 interview answers, about 12 minutes.

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

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:

Interactive · stage by stage The life of a standalone PyQGIS script Step through what a script must do before and after its own work, which the Python console does for you.
The life of a standalone PyQGIS script, step 6 of 6. Showing The environment, The prefix path, QgsApplication, initQgis(), The script's work, exitQgis(). In focus: exitQgis().The environmentQT_QPA_PLATFORM=offscreenThe prefix pathwhere QGIS is installedQgsApplication[] and False: no windowinitQgis()providers and registryThe script's worklayers, Processing, layoutsexitQgis()providers released

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. The prefix path. setPrefixPath tells QGIS where it is installed, so that it finds its data providers, its resources and its Processing plugins.
  3. The application. QgsApplication([], False) makes the QGIS application with no command-line arguments, and False means no window: the script needs no graphical interface.
  4. initQgis. initQgis() loads the data providers and the layer registry. Only now can the script open a GeoPackage or run an algorithm.
  5. The work. The script loads layers, asks for features, edits, runs Processing and exports layouts, with the same classes the QGIS window uses.
  6. 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 script, from QgsApplication to exitQgis Python · qgis/standalone.py
"""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

  1. Question 1 of 2 In which place does PyQGIS code have iface, the object that reaches QGIS's map, menus and toolbars?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In QGIS's Python console, inside the running QGIS

    Code that needs the window belongs in the console or a plugin; code that must run on a schedule is a standalone script.

  2. Question 2 of 2 A standalone script calls QgsApplication([], False). What does False mean?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No graphical interface, so the script needs no window

    The empty list is the command-line arguments; False asks for no window. On a server, QT_QPA_PLATFORM=offscreen lets Qt draw without a screen.

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 on the town's households Python · qgis/requests.py
"""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

  1. Question 1 of 2 A loop over every household read 5,000 features; the request with W04's rectangle read 1,169. What share of the features did the rectangle read, in per cent?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 23.38 % (anything from 23.33 to 23.43 counts)

    1,169 ÷ 5,000 = 0.2338, so 23.38 %: the request skipped three features in four that the task did not need.

  2. Question 2 of 2 You ask for households with NoGeometry and a rectangle filter at the same time. What did QGIS do in the lesson's run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It read the geometries anyway, because the box test needs them

    NoGeometry saves reading shapes when nothing needs them; a spatial filter needs them, so it reads them.

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:

An edit session that commits, one that rolls back, and a direct write Python · qgis/edit_session.py
"""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

  1. One question Inside with edit(wards):, a loop sets area_km2 to 0 and raises an error at W02. What does the file hold afterwards?

    Choose one answer.

    Show the answer: One question

    Answer: The old values, 4.0 for every ward, because the session rolled back

    Changes in an edit session reach the file together, at the commit, or not at all.

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):

Two algorithms run from Python Python · qgis/processing_from_python.py
"""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

  1. One question What does processing.run return?

    Choose one answer.

    Show the answer: One question

    Answer: A dictionary of the algorithm's outputs, such as {'OUTPUT': 'near.gpkg'}

    Read each output from the dictionary by its name, as the toolbox and qgis_process name it.

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.

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:

  1. for each ward in wards, in its order: W05: missing if no row has it, W05: 2 rows if more than one does;
  2. for each row, in its order: W07: near 400 is more than households 379 when near is larger; otherwise W02: share_pct 43.0, but near / households gives 43.7 when share_pct differs from round(100 * near / households, 1) (write both numbers with one decimal);
  3. 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.

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.

  1. Question 1 of 9 Which two modules does QGIS's Python console import for you when it starts?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: qgis.core and qgis.utils

    In a standalone script you import everything yourself, after making sure Python can find qgis.

  2. Question 2 of 9 What does exitQgis() do at the end of a standalone script?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: It removes the data providers and the layer registry from memory before Python exits

    Call it last, after your layers and results are finished with.

  3. Question 3 of 9 You request the households inside a ward shaped like an L, with setFilterRect on the ward's bounding box. What else do you need?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: A test of each household against the ward's own shape, such as setDistanceWithin(ward, 0)

    For W04, a square, the box and the ward agree; for most wards they do not, so let the box find the candidates and the ward's own shape decide.

  4. Question 4 of 9 What did QgsFeatureRequest().setLimit(3) return in the lesson?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: The first three households, H0001, H0002 and H0003

    Use a limit for previews and quick tests, never for a count that must be complete.

  5. Question 5 of 9 A script needs the names of all 2 million addresses in a district, to check their spelling. Which request reads the least?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: A subset of one attribute, with the NoGeometry flag

    Ask for exactly what the task uses: here one field and no geometry.

  6. Question 6 of 9 What happens when a script changes a value through layer.dataProvider()?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The change is written to the file at once, with no undo

    Use the provider for writes that must happen at once, and an edit session for anything that may need rolling back.

  7. Question 7 of 9 A script recalculates a field for 12,000 parcels and may stop halfway if a check fails. How should it write the changes?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: In an edit session with with edit(layer):, so a failure leaves the file as it was

    All or nothing is what an edit session gives a script that may stop halfway.

  8. Question 8 of 9 Which name does processing.run take for Extract within distance?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: native:extractwithindistance

    processing.algorithmHelp(id) lists an algorithm's parameters before you call it.

  9. Question 9 of 9 The lesson passed OUTPUT 'memory:' to Extract within distance and 'near.gpkg' to Count points in polygon. What is the difference?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: memory: keeps the result as a temporary layer in the script; near.gpkg writes a file that outlasts it

    Keep in-between results in memory and write only what someone will use.

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:

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.