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.

The module layers, and where the display requirement startsThe core module sits at the base and needs no display. Analysis and the Processing framework build on core and also run headless. The GUI module sits above and requires a graphical environment, and the utils module provides access to the running QGIS application and its interface object, which exists only inside QGIS desktop. A dividing line marks which layers a headless script may use.The line between these two halves is where headless scripts breakqgis.core — layers, geometry, CRS, projects, renderingno display needed, safe everywhere, most of the APIqgis.analysisnetworks, interpolation, raster mathsprocessingalgorithms, models, providersabove this line: a display or a running QGIS is requiredqgis.guicanvas, map tools, widgetsqgis.utils — iface, pluginsonly inside QGIS desktopKeep the work in the lower half and the same code runs in the console, a plugin and a cron job

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.

The shape that runs in both worldsOn the left, a function reaches for the active layer and pushes messages to the message bar, so it only works inside QGIS desktop. On the right, the same work is a plain function taking a layer and returning a result, wrapped by a thin plugin layer that supplies the layer and displays the result. Only the second version can also be called by a scheduled job.Push the interface to the edges and the middle becomes reusableinterface woven throughlayer = iface.activeLayer()do the analysisiface.messageBar().push(...)cannot be scheduledinterface at the edgesplugin: get the layer from ifaceanalyse(layer) — core onlyplugin: display the resultplugin, console and cron

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.

Why the import path matters more than the class nameAn import through qgis.PyQt resolves at run time to the Qt binding the running QGIS was built against, so the same code works on a Qt 5 build and a Qt 6 build. A direct import from PyQt5 works only where that binding is present, and fails to load the plugin entirely on any other build.One import line decides whether the plugin is portablefrom qgis.PyQt import ...resolved at run timefrom PyQt5 import ...pinned at authoring timea Qt 5 build of QGISboth imports worka Qt 6 build of QGISonly the shim worksthe dashed path stops here — the plugin never loads

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 versionPythonNotes
3.22 LTR3.9Module layout as described; qgis.PyQt resolves to PyQt5.
3.28 LTR3.9Identical.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12Same 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

  • ImportError on qgis.gui in a headless script. Correct behaviour — the script should not need it. Move the GUI code into the plugin layer.
  • iface is None. 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 PyQt5 import. Change it to qgis.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) and dir(qgis.analysis).
  • Everything works interactively and nothing works on the server. Almost always a GUI or iface dependency 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.