QGIS Core, GUI and Analysis Modules Explained
Every PyQGIS script begins with an import, and the module you import from tells you more than where a class lives: it tells you whether the code can run without a screen, whether it belongs in a plugin or in a batch job, and whether it will still work when somebody runs it on a server. Getting this wrong is how a working script becomes an unattended job that fails at three in the morning with an error about a display.
This recipe belongs to QGIS API Architecture. It maps the modules — qgis.core, qgis.gui, qgis.analysis, qgis.utils, qgis.PyQt and processing — and shows how to structure code so the part that does the work never depends on the part that draws.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- The Python console, or an editor set up as in Setting Up PyCharm for QGIS.
- No other setup — this page is about where things live.
qgis.core: almost everything
from qgis.core import (
QgsApplication, QgsProject, QgsVectorLayer, QgsRasterLayer,
QgsFeature, QgsGeometry, QgsPointXY,
QgsCoordinateReferenceSystem, QgsCoordinateTransform,
QgsField, QgsFeatureRequest, QgsExpression,
QgsVectorFileWriter, QgsSettings,
)
Breakdown: If a class represents data, a transformation, a setting or a file operation, it is in qgis.core — layers, features, geometry, coordinate systems, projects, expressions, symbols, renderers, layouts and the whole rendering engine. None of it requires a display, which is why a well-structured script can do all of its real work with qgis.core imports only. When you are unsure where a class lives, qgis.core is the correct first guess and is right most of the time.
The one thing that surprises people is that rendering lives here too: producing an image from layers, exporting a layout to PDF, drawing a map to a file. Rendering is not the same as displaying, and QGIS keeps the distinction cleanly, which is precisely what makes headless map production possible.
qgis.gui: only where there is a screen
from qgis.gui import (
QgsMapCanvas, QgsMapTool, QgsRubberBand,
QgsMapLayerComboBox, QgsFileWidget, QgsOptionsPageWidget,
)
Breakdown: Anything the user interacts with — the canvas, map tools, rubber bands, the layer combo boxes and file widgets you put in plugin dialogs — lives in qgis.gui. Importing this module in a script running without a display can fail outright or, worse, succeed and then crash the first time a widget is constructed. In a plugin it is entirely appropriate; in a batch job it is a design error, and the fastest way to make a script that "works on my machine" and nowhere else. The widgets themselves are covered in Qt Designer for GIS Interfaces and the map tools in Create a Custom Map Tool in PyQGIS.
qgis.analysis and processing
from qgis.analysis import QgsNativeAlgorithms, QgsZonalStatistics, QgsGraphBuilder
import processing
from processing.core.Processing import Processing
Breakdown: qgis.analysis holds the analytical machinery that is not simply data handling: network analysis and shortest paths, interpolation, zonal statistics, geometry checking. Much of it is also exposed as Processing algorithms, and calling the algorithm is usually the better choice — it handles progress, cancellation and output creation for you. processing is the framework's Python interface, and in a standalone script it needs initialising explicitly along with the native algorithm provider, which is one of the steps described in Running Python Scripts Outside QGIS Desktop. Both run happily without a display.
qgis.utils: the running application
from qgis.utils import iface, plugins, reloadPlugin
print(iface.activeLayer())
print(list(plugins.keys()))
Breakdown: qgis.utils is the bridge to a running QGIS desktop: iface is the interface object that gives access to the canvas, the menus, the message bar and the active layer, and plugins is the dictionary of loaded plugins by folder name. None of it exists outside QGIS desktop — in a standalone script iface is None, and code that assumes otherwise fails immediately. This is the single most common reason a console script cannot be scheduled: it reaches for iface.activeLayer() instead of taking the layer as a parameter.
qgis.PyQt: import Qt through QGIS
from qgis.PyQt.QtCore import QCoreApplication, QVariant, Qt
from qgis.PyQt.QtWidgets import QAction, QMessageBox
from qgis.PyQt.QtGui import QColor, QIcon
Breakdown: Always import Qt classes through qgis.PyQt rather than directly from PyQt5. The shim resolves to whichever Qt binding the running QGIS was built against, which is what lets the same plugin work on a Qt 5 build and a Qt 6 build without a change. Direct from PyQt5.QtWidgets import ... imports work today and break on the next major QGIS release; they are also the reason a plugin fails to load on someone else's platform while working perfectly on yours.
Structure code so the work is portable
The practical payoff of understanding the modules is a shape: keep the work in plain functions that import only qgis.core, qgis.analysis and processing, and confine qgis.gui and qgis.utils to a thin outer layer.
# analysis.py — no GUI, no iface, testable and schedulable
from qgis.core import QgsVectorLayer, QgsFeatureRequest
def summarise_areas(layer, minimum_area=0.0):
request = QgsFeatureRequest().setSubsetOfAttributes(["ref"], layer.fields())
return {
feature["ref"]: feature.geometry().area()
for feature in layer.getFeatures(request)
if feature.geometry().area() >= minimum_area
}
# plugin.py — the only file that knows about the interface
from qgis.utils import iface
from .analysis import summarise_areas
def run():
layer = iface.activeLayer()
if layer is None:
iface.messageBar().pushWarning("Parcel Tools", "Select a layer first")
return
results = summarise_areas(layer)
iface.messageBar().pushInfo("Parcel Tools", f"{len(results)} parcels summarised")
Breakdown: The analysis function takes a layer and returns data, which makes it callable from a plugin, from the console, from a scheduled script and from a unit test without any of them needing a display. The plugin file is the only place that imports qgis.utils, and it does nothing but fetch input and report output. This separation costs one extra file and pays for itself the first time somebody asks whether the tool can run nightly — the answer is yes, with no rewrite. It is also what makes the test setup in Unit Test a QGIS Plugin with pytest straightforward rather than an exercise in mocking the entire application.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | Module layout as described; qgis.PyQt resolves to PyQt5. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Same layout; builds against Qt 6 exist, which is exactly why qgis.PyQt rather than PyQt5 matters. |
Classes do occasionally move between modules across major versions — QgsMapLayerComboBox and friends have always been GUI, but some analysis classes migrated into core. dir() and the API documentation settle it in seconds, using the introspection covered in Explore the PyQGIS API with dir() and help().
Troubleshooting
ImportErroronqgis.guiin a headless script. Correct behaviour — the script should not need it. Move the GUI code into the plugin layer.ifaceisNone. You are not inside QGIS desktop. Pass the layer in as an argument instead of reaching for the active one.- A plugin fails to load on another platform. A direct
PyQt5import. Change it toqgis.PyQt. processing.run()reports no algorithms. In a standalone script, Processing and the native provider must be initialised explicitly before use.- A class cannot be imported from where a tutorial says. Either QGIS 2 code, or the class has moved. Check
dir(qgis.core)anddir(qgis.analysis). - Everything works interactively and nothing works on the server. Almost always a GUI or
ifacedependency buried in a function that looks like pure logic.
Conclusion
qgis.core holds the data model and the rendering engine and needs no display; qgis.analysis and processing build on it and are equally portable; qgis.gui and qgis.utils require a running QGIS desktop. Import Qt through qgis.PyQt so the code survives a binding change, and structure scripts so the work lives in functions that take arguments and return values — the same code then runs in the console, in a plugin and in a scheduled job.
Frequently Asked Questions
Can I use qgis.gui classes in a Processing algorithm?
No. Algorithms must run headless and on background threads. Anything the user needs to choose becomes an algorithm parameter instead.
Why import Qt through qgis.PyQt?
So the code resolves to whichever Qt version the running QGIS uses. It is a one-word change that makes a plugin portable across builds.
Is processing part of qgis.core?
No — it is a separate Python package shipped with QGIS. In a standalone script it needs explicit initialisation, unlike in the console where QGIS has already done it.
Where do symbols and renderers live?
In qgis.core. Styling is data about how to draw, not interface, so it is usable headless — which is what makes automated cartography possible.
How do I know which module a class is in?
Try qgis.core first, then check the API documentation, or filter dir() on each module. The class page also states the module explicitly.