Use PyQGIS in a Jupyter Notebook
A notebook is a good fit for the kind of GIS work that is really analysis: try a buffer distance, look at the result, adjust, keep a record of what you did. PyQGIS runs there perfectly well — it is a normal Python library once the paths are right — and the result is a document combining code, tables, maps and the reasoning behind them, which is far more useful to a colleague than a script and a folder of outputs.
This recipe belongs to Virtual Environments for GIS. It covers making the QGIS libraries importable from a notebook kernel, initialising the application headless, rendering a map into a cell, and combining PyQGIS with the wider Python data stack without either environment interfering with the other.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer, installed on the machine.
- A notebook environment whose Python is the same minor version as the one QGIS uses — this is the constraint that decides whether any of this works.
- Jupyter itself, installed either into the QGIS Python or into an environment that can reach the QGIS libraries.
Make the QGIS libraries importable
The simplest arrangement is to install Jupyter into the QGIS Python, using the technique from Install Python Packages into the QGIS Environment. Everything then works with no path manipulation at all. Where that is not desirable, add the QGIS libraries to your own environment:
import os
import sys
QGIS_PREFIX = "/usr" # /Applications/QGIS.app/Contents/MacOS on macOS
sys.path.append(f"{QGIS_PREFIX}/share/qgis/python")
sys.path.append(f"{QGIS_PREFIX}/share/qgis/python/plugins") # for the processing package
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from qgis.core import QgsApplication
print(QgsApplication.qgisVersion() if hasattr(QgsApplication, "qgisVersion") else "imported")
Breakdown: Two folders matter: the Python bindings themselves and the plugins folder, which is where the processing package lives — omit the second and every import works except Processing, which is a confusing half-failure. QT_QPA_PLATFORM set to offscreen must be in place before Qt is imported, which is why it appears above the import rather than after it; without it, a kernel on a machine with no display fails with an error about not connecting to a display server. The prefix differs by platform, and on Windows it is the OSGeo4W root with its own set of environment variables, which makes installing Jupyter into the QGIS Python distinctly the easier route there.
Initialise the application once
from qgis.core import QgsApplication
qgs = QgsApplication([], False)
qgs.setPrefixPath(QGIS_PREFIX, True)
qgs.initQgis()
from processing.core.Processing import Processing
import processing
Processing.initialize()
print(len(QgsApplication.processingRegistry().algorithms()), "algorithms available")
Breakdown: QgsApplication([], False) creates the application with no arguments and no GUI. initQgis() loads the providers — without it, every layer you create is invalid for reasons the error message does not explain. Processing needs its own initialisation in a standalone context, and after it the registry reports several hundred algorithms; a count of zero means the plugins folder is missing from the path. Run this cell exactly once per kernel: calling initQgis() twice is undefined behaviour, so guard it or simply restart the kernel when things get confused. The equivalent standalone-script setup is covered in Running Python Scripts Outside QGIS Desktop.
Render a map into a cell
The payoff of a notebook is seeing the result. Rendering to an image and displaying it inline takes a dozen lines and works with no display attached.
from IPython.display import Image
from qgis.core import (QgsVectorLayer, QgsProject, QgsMapSettings,
QgsMapRendererParallelJob)
from qgis.PyQt.QtCore import QSize
from qgis.PyQt.QtGui import QColor
layer = QgsVectorLayer("/data/city.gpkg|layername=parcels", "Parcels", "ogr")
QgsProject.instance().addMapLayer(layer)
settings = QgsMapSettings()
settings.setLayers([layer])
settings.setBackgroundColor(QColor("white"))
settings.setOutputSize(QSize(800, 500))
settings.setExtent(layer.extent())
job = QgsMapRendererParallelJob(settings)
job.start()
job.waitForFinished()
job.renderedImage().save("/tmp/map.png", "PNG")
Image("/tmp/map.png")
Breakdown: QgsMapSettings describes what to draw, at what size and over what extent — it is the same object the canvas uses, which is why the output matches what QGIS desktop would show. QgsMapRendererParallelJob renders on worker threads; waitForFinished() blocks until it is done, which is what you want in a notebook where the next cell expects the file to exist. Saving to a file and displaying it is more reliable than converting the image in memory, and it leaves an artefact you can put in a report. Re-running the cell after changing the styling gives the map-adjust-look loop that makes notebooks worth using for cartography experiments.
Combine PyQGIS with the data stack
Attributes are more convenient as a data frame, and getting them there is a comprehension:
import pandas as pd
records = [
{**dict(zip(layer.fields().names(), feature.attributes())),
"area_m2": feature.geometry().area()}
for feature in layer.getFeatures()
]
frame = pd.DataFrame.from_records(records)
frame.groupby("ward")["area_m2"].sum().sort_values(ascending=False).head()
Breakdown: Zipping field names with attribute values gives a dictionary per feature, and adding a computed geometry column in the same step avoids a second pass. From there the whole of pandas applies: grouping, joining to a spreadsheet, plotting a distribution. The traffic works both ways — a result computed in pandas can be written back as a new field, or exported to CSV and joined in QGIS. For large layers, narrow the read with a feature request first rather than materialising every attribute, following Speed Up Feature Iteration with QgsFeatureRequest.
Exit cleanly and keep notebooks reproducible
qgs.exitQgis() releases the providers and should be called at the end of a session, though in practice a notebook kernel is usually just restarted. The more important discipline is the one every notebook needs and GIS notebooks need more: restart the kernel and run all cells before sharing. A notebook that only works because of a layer created in a cell that was later deleted is worse than a broken script, because it looks like it worked.
Two more habits pay off. Keep the setup — paths, initialisation, imports — in the first cell and nothing else, so the reproducibility question has one answer. And write outputs to files with explicit names rather than leaving them in memory, so the notebook produces artefacts a colleague can check without running anything.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | Works; the kernel's Python must also be 3.9. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Identical setup; no notebook-specific changes. |
The binding constraint is always the Python minor version: PyQGIS is a compiled extension built for one version, and a 3.12 build cannot be imported by a 3.10 kernel. Where your data-science environment is pinned elsewhere, run QGIS work in its own kernel and exchange files.
Troubleshooting
ModuleNotFoundError: qgis. The bindings folder is not on the path, or the kernel's Python version does not match QGIS's.ImportErrorforprocessing. The QGIS plugins folder is missing fromsys.path.- A display error on a headless machine. Set
QT_QPA_PLATFORM=offscreenbefore importing anything fromqgisorPyQt. - Layers are always invalid.
initQgis()was never called, or the prefix path is wrong so providers were not found. - The kernel dies when a cell runs. Usually a second
QgsApplication, or a mismatched GDAL between the environment and QGIS. Restart and initialise once. processingRegistry().algorithms()is empty.Processing.initialize()was not called, or the native provider is not registered.
Conclusion
Point the kernel at the QGIS Python libraries and the plugins folder, set the offscreen platform before Qt loads, create and initialise one QgsApplication, and initialise Processing. From there PyQGIS is an ordinary library: build layers, run algorithms, render maps to inline images and move attributes into a data frame. Keep the setup in the first cell, and always restart-and-run-all before sharing.
Frequently Asked Questions
Should I install Jupyter into the QGIS Python or the other way round? Installing Jupyter into the QGIS Python is far simpler and avoids all path manipulation. Reach for the other arrangement only when the notebook environment has dependencies you cannot install alongside QGIS.
Can I use the map canvas in a notebook?
No — the canvas is a GUI widget. Render with QgsMapSettings and a renderer job instead, which produces the same output as an image.
Does this work in JupyterLab and VS Code notebooks? Yes. The kernel is what matters; the front end is irrelevant.
Can I run this on a server with no display? Yes, that is exactly what the offscreen platform is for. It is the same arrangement as Run PyQGIS in a Docker Container.
How do I show an interactive map? Export the layer to GeoJSON and use a web-mapping library in the notebook. PyQGIS renders images; interactivity comes from the notebook side.
Is a notebook a good place for production automation? No. Notebooks are for exploration; when the workflow settles, move it into a script that can be scheduled and tested.