
Qgis Core Architecture
- 8 installs
- 29 repo stars
- Updated July 8, 2026
- openaec-foundation/qgis-claude-skill-package
Helps with ai & agent building tasks.
About
qgis-core-architecture is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- qgis-core-architecture
- AI & Agent Building
- AI-coding skill
Qgis Core Architecture by the numbers
- 8 all-time installs (skills.sh)
- Ranked #12,339 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/qgis-claude-skill-package --skill qgis-core-architectureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 29 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/qgis-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
qgis-core-architecture
Quick Reference
Core Python Modules (QGIS 3.x)
| Module | Purpose |
|---|---|
qgis.core | All non-GUI functionality: layers, features, geometry, CRS, project, processing, data providers |
qgis.gui | GUI components: map canvas (QgsMapCanvas), layer tree view, attribute forms |
qgis.analysis | Spatial analysis: network analysis, raster calculator, interpolation |
qgis.server | QGIS Server for OGC web services (WMS, WFS, WCS) |
qgis._3d | 3D map visualization components |
Additional utility modules:
qgis.utils-- Automatically imported in the QGIS Python console; provides helper functionsqgis.PyQt-- Re-exports PyQt5/PyQt6 classes for version-independent imports
Layer Type Hierarchy
| Class | Type Enum | Description |
|---|---|---|
QgsVectorLayer | VectorLayer | Points, lines, polygons with attributes |
QgsRasterLayer | RasterLayer | Grid-based data (GeoTIFF, WMS, XYZ tiles) |
QgsMeshLayer | MeshLayer | Unstructured mesh data (hydrodynamic models) |
QgsPointCloudLayer | PointCloudLayer | LiDAR / point cloud data (LAS, LAZ, COPC, EPT) |
QgsAnnotationLayer | AnnotationLayer | Freeform annotations on the map |
QgsVectorTileLayer | VectorTileLayer | Mapbox Vector Tiles (MVT) |
QgsTiledSceneLayer | TiledSceneLayer | 3D tiled scenes (3D Tiles, Cesium) |
All layer types inherit from QgsMapLayer. Access the type via layer.type().
Project File Formats
| Format | Extension | Description |
|---|---|---|
| QGS | .qgs | XML-based project file; human-readable, larger file size |
| QGZ | .qgz | Compressed ZIP archive containing .qgs and auxiliary data; default since QGIS 3.2 |
Key Dependencies
| Component | Role |
|---|---|
| Qt5 / Qt6 | Application framework (Qt6 in QGIS 4.x) |
| SIP | C++ to Python binding generator (tighter Qt integration than SWIG) |
| PyQt5 / PyQt6 | Python Qt bindings |
| GDAL/OGR | Raster and vector data I/O |
| PROJ | Coordinate reference system transformations |
| GEOS | Geometry operations |
---
Critical Warnings
NEVER access QgsProject.instance() from background threads without proper synchronization. The project singleton is NOT thread-safe for writes.
NEVER modify GUI elements (map canvas, layer tree, dialogs) from background threads. ALWAYS use QgsTask and emit signals to communicate results back to the main thread.
NEVER create layers on a background thread and add them to the project directly. Create the data on the background thread, then add the layer on the main thread via signal.
NEVER assume a layer is valid without calling isValid(). A layer can be created without errors but still be invalid (wrong path, missing provider, auth failure).
NEVER access features or data provider methods on an invalid layer -- this leads to crashes or undefined behavior.
NEVER call QgsProject.instance().clear() without confirming that unsaved changes are acceptable -- this destroys all loaded layers and settings immediately.
NEVER rely on QgsProject.instance().mapLayersByName() returning a single result -- layer names are NOT unique. ALWAYS index into the returned list and handle empty lists.
NEVER use backslashes in paths even on Windows -- QGIS/Qt normalizes to forward slashes internally. Using backslashes in URIs causes provider failures.
NEVER call QgsApplication([], False) without first calling QgsApplication.setPrefixPath() -- providers will fail to load.
ALWAYS call qgs.exitQgis() at the end of standalone scripts to prevent memory leaks.
ALWAYS check layer validity immediately after creation with layer.isValid().
---
Decision Tree
Which Initialization Context?
Are you writing code inside the QGIS application?
├── YES (plugin or Python console)
│ └── QgsApplication is ALREADY initialized
│ ├── `iface` is available (QgisInterface)
│ ├── `QgsProject.instance()` is ready
│ └── Do NOT call QgsApplication() or initQgis()
├── NO (standalone script, no GUI)
│ └── MUST initialize QgsApplication manually:
│ 1. QgsApplication.setPrefixPath(path, True)
│ 2. qgs = QgsApplication([], False)
│ 3. qgs.initQgis()
│ 4. ... your code ...
│ 5. qgs.exitQgis()
└── NO (standalone script, with GUI)
└── Same as above but pass True to QgsApplication:
1. QgsApplication.setPrefixPath(path, True)
2. qgs = QgsApplication([], True)
3. qgs.initQgis()
4. ... your GUI code ...
5. qgs.exitQgis()Which Layer Class?
What kind of data are you loading?
├── Vector data (points, lines, polygons) → QgsVectorLayer
├── Raster/grid data (GeoTIFF, DEM) → QgsRasterLayer
├── Mesh data (hydrodynamic models) → QgsMeshLayer
├── Point cloud (LAS, LAZ, COPC) → QgsPointCloudLayer
├── Vector tiles (MVT) → QgsVectorTileLayer
├── 3D tiled scenes (Cesium 3D Tiles) → QgsTiledSceneLayer
└── Map annotations → QgsAnnotationLayerWhich Data Provider?
What is your data source?
├── Local vector file (.shp, .gpkg, .geojson, .fgb, .kml) → provider: "ogr"
├── Local raster file (.tif, .jp2, .vrt) → provider: "gdal"
├── PostgreSQL/PostGIS database → provider: "postgres"
├── SpatiaLite database → provider: "spatialite"
├── In-memory layer → provider: "memory"
├── Delimited text file (.csv with coordinates) → provider: "delimitedtext"
├── WFS web service → provider: "WFS"
├── WMS/WMTS web service → provider: "wms"
├── WCS web service → provider: "wcs"
├── GPX file → provider: "gpx"
└── Virtual layer (SQL over layers) → provider: "virtual"---
Essential Patterns
Standalone Script Initialization (No GUI)
from qgis.core import QgsApplication, QgsVectorLayer, QgsProject
QgsApplication.setPrefixPath("/path/to/qgis/installation", True)
qgs = QgsApplication([], False)
qgs.initQgis()
# Load and work with layers
layer = QgsVectorLayer("data/airports.gpkg|layername=airports", "Airports", "ogr")
if not layer.isValid():
raise RuntimeError("Layer failed to load")
QgsProject.instance().addMapLayer(layer)
# ALWAYS clean up
qgs.exitQgis()Plugin Entry Point Pattern
class MyPlugin:
def __init__(self, iface):
self.iface = iface # QgisInterface instance
def initGui(self):
# Register actions, menus, toolbars
self.action = QAction("My Plugin", self.iface.mainWindow())
self.action.triggered.connect(self.run)
self.iface.addToolBarIcon(self.action)
def unload(self):
# ALWAYS clean up all registered elements
self.iface.removeToolBarIcon(self.action)
def run(self):
# Plugin logic here
layer = self.iface.activeLayer()
if layer is not None and layer.isValid():
# Process layer
pass
def classFactory(iface):
return MyPlugin(iface)Project Operations
project = QgsProject.instance()
# Load project
success = project.read("/path/to/project.qgz")
# Load project skipping layer resolution (fast, for metadata access)
readflags = Qgis.ProjectReadFlags()
readflags |= Qgis.ProjectReadFlag.DontResolveLayers
project.read("/path/to/project.qgs", readflags)
# Save project
project.write() # Save to current location
project.write("/path/to/new_project.qgz") # Save to new path
# Access all layers (returns dict of {id: layer})
all_layers = project.mapLayers()
# Find layers by name (returns list -- names are NOT unique)
layers = project.mapLayersByName("Airports")
if not layers:
raise RuntimeError("Layer 'Airports' not found")
layer = layers[0]Layer Creation and Validation
# Vector layer
vlayer = QgsVectorLayer("data/airports.shp", "Airports", "ogr")
if not vlayer.isValid():
raise RuntimeError(f"Failed to load layer: {vlayer.name()}")
QgsProject.instance().addMapLayer(vlayer)
# Raster layer
rlayer = QgsRasterLayer("data/srtm.tif", "SRTM", "gdal")
if not rlayer.isValid():
raise RuntimeError(f"Failed to load raster: {rlayer.name()}")
QgsProject.instance().addMapLayer(rlayer)
# Memory layer (for temporary data)
mem_layer = QgsVectorLayer("Point?crs=EPSG:4326", "Temp Points", "memory")Background Processing with QgsTask
from qgis.core import QgsTask, QgsApplication
class HeavyProcessingTask(QgsTask):
def __init__(self, description, layer_id):
super().__init__(description, QgsTask.CanCancel)
self.layer_id = layer_id
self.result_data = None
def run(self):
# Runs on background thread -- NO GUI access here
# NO QgsProject.instance() writes here
self.result_data = self._process_data()
return True
def finished(self, result):
# Runs on main thread -- safe to update GUI and project
if result:
# Add results to project here
pass
task = HeavyProcessingTask("Processing data", layer.id())
QgsApplication.taskManager().addTask(task)Signal/Slot Connections
# Connect to project layer changes
QgsProject.instance().layersAdded.connect(on_layers_added)
QgsProject.instance().layersRemoved.connect(on_layers_removed)
# Connect to map canvas render
iface.mapCanvas().renderComplete.connect(on_render_complete)
# ALWAYS disconnect when done (e.g., in plugin unload)
QgsProject.instance().layersAdded.disconnect(on_layers_added)
iface.mapCanvas().renderComplete.disconnect(on_render_complete)---
Common Operations
Path Resolution
# Rewrite paths during loading (e.g., migrating between machines)
def my_load_preprocessor(path):
return path.replace("c:/Users/Old/", "x:/New/")
preprocessor_id = QgsPathResolver.setPathPreprocessor(my_load_preprocessor)
# Remove preprocessor when no longer needed
QgsPathResolver.removePathPreprocessor(preprocessor_id)Layer Tree Management
root = QgsProject.instance().layerTreeRoot()
# Create a group
group = root.addGroup("Analysis Results")
# Add layer to specific group (not root)
QgsProject.instance().addMapLayer(layer, False) # False = don't add to root
group.addLayer(layer)
# Find group or layer node
my_group = root.findGroup("Analysis Results")
node = root.findLayer(layer.id())Canvas-Project Bridge (Standalone GUI Apps)
from qgis.core import QgsProject
from qgis.gui import QgsLayerTreeMapCanvasBridge
bridge = QgsLayerTreeMapCanvasBridge(
QgsProject.instance().layerTreeRoot(),
canvas
)
project.read("/path/to/project.qgs")---
QGIS 4.0 Migration Notes (Qt6)
QMetaType.Type Replaces QVariant.Type
# QGIS 3.x (Qt5) -- WILL BREAK in QGIS 4.x
from qgis.PyQt.QtCore import QVariant
field = QgsField("name", QVariant.String)
# QGIS 4.x (Qt6) -- Forward-compatible
from qgis.PyQt.QtCore import QMetaType
field = QgsField("name", QMetaType.Type.QString)| Qt5 (QVariant.Type) | Qt6 (QMetaType.Type) |
|---|---|
QVariant.String | QMetaType.Type.QString |
QVariant.Int | QMetaType.Type.Int |
QVariant.Double | QMetaType.Type.Double |
QVariant.Bool | QMetaType.Type.Bool |
QVariant.Date | QMetaType.Type.QDate |
QVariant.DateTime | QMetaType.Type.QDateTime |
QVariant.LongLong | QMetaType.Type.LongLong |
Other Qt6 Breaking Changes
QRegExpremoved -- useQRegularExpressioninsteadexec_()methods renamed toexec()(dialogs, event loops)- Enum scoping changes -- unscoped enums become scoped
- Use
qgis.PyQtimports to abstract differences between PyQt5 and PyQt6
---
Reference Links
- references/methods.md -- API signatures for QgsProject, QgsApplication, QgsMapLayer, QgsProviderRegistry
- references/examples.md -- Working code examples verified against PyQGIS 3.44 documentation
- references/anti-patterns.md -- What NOT to do, with WHY explanations
Official Sources
- https://docs.qgis.org/latest/en/docs/pyqgis_developer_cookbook/intro.html
- https://docs.qgis.org/latest/en/docs/pyqgis_developer_cookbook/loadproject.html
- https://docs.qgis.org/latest/en/docs/pyqgis_developer_cookbook/loadlayer.html
- https://qgis.org/pyqgis/3.44/
Anti-Patterns (QGIS Architecture)
Initialization Anti-Patterns
1. Missing setPrefixPath Before initQgis
# WRONG: initQgis() without setPrefixPath -- providers fail to load
qgs = QgsApplication([], False)
qgs.initQgis()
# CORRECT: ALWAYS set prefix path first
QgsApplication.setPrefixPath("/usr/share/qgis", True)
qgs = QgsApplication([], False)
qgs.initQgis()WHY: Without the prefix path, QGIS cannot locate its provider plugins, CRS database, or authentication database. Layers will fail to load with cryptic "unknown provider" errors.
---
2. Initializing QgsApplication Inside a Plugin
# WRONG: Calling initQgis() inside a QGIS plugin or console
def initGui(self):
qgs = QgsApplication([], False)
qgs.initQgis() # Application is ALREADY initialized!
# CORRECT: In plugins, the application is already running
def initGui(self):
# QgsProject.instance() and iface are ready to use
project = QgsProject.instance()WHY: QGIS is already initialized when your plugin loads. Creating a second QgsApplication instance causes undefined behavior including crashes and corrupted state.
---
3. Forgetting exitQgis in Standalone Scripts
# WRONG: No cleanup -- memory leak
QgsApplication.setPrefixPath("/usr/share/qgis", True)
qgs = QgsApplication([], False)
qgs.initQgis()
# ... do work ...
# Script ends without exitQgis()
# CORRECT: ALWAYS clean up
QgsApplication.setPrefixPath("/usr/share/qgis", True)
qgs = QgsApplication([], False)
qgs.initQgis()
try:
# ... do work ...
pass
finally:
qgs.exitQgis()WHY: Without exitQgis(), provider and layer registries remain in memory. In long-running scripts or repeated executions, this causes memory leaks and potential resource exhaustion.
---
Threading Anti-Patterns
4. Modifying QgsProject from a Background Thread
# WRONG: Adding a layer from a background thread
class BadTask(QgsTask):
def run(self):
layer = QgsVectorLayer("data/points.shp", "Points", "ogr")
QgsProject.instance().addMapLayer(layer) # CRASH or corruption
return True
# CORRECT: Create data on background thread, add layer on main thread
class GoodTask(QgsTask):
def __init__(self):
super().__init__("Safe Task", QgsTask.CanCancel)
self.result_data = None
def run(self):
# Process data here -- NO project writes
self.result_data = self._compute_results()
return True
def finished(self, result):
# Runs on main thread -- safe to modify project
if result:
layer = QgsVectorLayer("Point?crs=EPSG:4326", "Results", "memory")
QgsProject.instance().addMapLayer(layer)WHY: QgsProject.instance() is NOT thread-safe for write operations. Writing from a background thread causes data corruption, crashes, or silently broken state.
---
5. Updating GUI from a Background Thread
# WRONG: Updating map canvas from a background thread
class BadTask(QgsTask):
def run(self):
iface.mapCanvas().refresh() # GUI access from background thread!
return True
# CORRECT: Use finished() or signals for GUI updates
class GoodTask(QgsTask):
def run(self):
# Data processing only
return True
def finished(self, result):
# Main thread -- safe to update GUI
iface.mapCanvas().refresh()WHY: Qt requires ALL GUI operations to execute on the main thread. Accessing GUI objects from background threads causes crashes, rendering artifacts, or deadlocks.
---
Layer Anti-Patterns
6. Skipping Layer Validity Check
# WRONG: Using a layer without validity check
layer = QgsVectorLayer("data/missing_file.shp", "Layer", "ogr")
for feature in layer.getFeatures(): # Crash or undefined behavior
print(feature)
# CORRECT: ALWAYS check isValid() after creation
layer = QgsVectorLayer("data/missing_file.shp", "Layer", "ogr")
if not layer.isValid():
raise RuntimeError(f"Failed to load layer from: {layer.source()}")
for feature in layer.getFeatures():
print(feature)WHY: A layer constructor NEVER raises an exception even if the data source is invalid. The layer object is created but in an invalid state. Accessing features or provider methods on an invalid layer causes crashes or returns garbage data.
---
7. Assuming mapLayersByName Returns One Result
# WRONG: Assuming a single result
layer = QgsProject.instance().mapLayersByName("Roads") # Returns a LIST
layer.getFeatures() # TypeError: list has no attribute getFeatures
# ALSO WRONG: Indexing without empty check
layer = QgsProject.instance().mapLayersByName("Roads")[0] # IndexError if empty
# CORRECT: Handle list result and empty case
layers = QgsProject.instance().mapLayersByName("Roads")
if not layers:
raise RuntimeError("Layer 'Roads' not found in project")
layer = layers[0]WHY: Layer names are NOT unique in QGIS. Multiple layers can share the same name. mapLayersByName() ALWAYS returns a list, even for zero or one matches.
---
8. Forgetting updateExtents After Adding Features
# WRONG: Extent is stale after adding features
mem_layer = QgsVectorLayer("Point?crs=EPSG:4326", "Points", "memory")
provider = mem_layer.dataProvider()
provider.addFeatures(features)
# layer.extent() returns empty/wrong extent
# "Zoom to Layer" does nothing useful
# CORRECT: ALWAYS update extents after modifying features
mem_layer = QgsVectorLayer("Point?crs=EPSG:4326", "Points", "memory")
provider = mem_layer.dataProvider()
provider.addFeatures(features)
mem_layer.updateExtents() # Recalculates bounding boxWHY: Memory layers do not automatically recalculate their extent when features are added via the data provider. Without updateExtents(), the layer reports an incorrect bounding box, causing "Zoom to Layer" to fail and spatial queries to miss features.
---
9. Forgetting updateFields After Adding Attributes
# WRONG: Field list is stale after adding attributes
provider = layer.dataProvider()
provider.addAttributes([QgsField("new_col", QVariant.String)])
print(layer.fields().names()) # "new_col" is MISSING from this list
# CORRECT: ALWAYS call updateFields after modifying field structure
provider = layer.dataProvider()
provider.addAttributes([QgsField("new_col", QVariant.String)])
layer.updateFields()
print(layer.fields().names()) # "new_col" is now presentWHY: The layer caches its field list for performance. When you modify fields through the data provider directly, the layer's cached field list becomes stale. updateFields() forces a refresh from the provider.
---
Path Anti-Patterns
10. Using Backslashes in Paths
# WRONG: Backslashes cause provider failures on all platforms
layer = QgsVectorLayer("C:\\data\\points.shp", "Points", "ogr")
# CORRECT: ALWAYS use forward slashes
layer = QgsVectorLayer("C:/data/points.shp", "Points", "ogr")
# ALSO CORRECT: Use os.path or pathlib for construction
import os
path = os.path.join("C:/data", "points.shp")
layer = QgsVectorLayer(path, "Points", "ogr")WHY: QGIS and Qt normalize all paths to forward slashes internally. Backslashes in URIs are not consistently handled by all data providers and cause failures, especially with GeoPackage sublayer URIs that use the | separator.
---
11. Hardcoding Absolute Paths
# WRONG: Hardcoded paths break on other machines
layer = QgsVectorLayer("/home/john/projects/data/roads.shp", "Roads", "ogr")
# CORRECT: Use project-relative paths
import os
data_dir = os.path.join(QgsProject.instance().homePath(), "data")
layer_path = os.path.join(data_dir, "roads.shp")
layer = QgsVectorLayer(layer_path, "Roads", "ogr")WHY: Hardcoded absolute paths make projects non-portable. When the project moves to another machine or user, all layer paths break. Using QgsProject.instance().homePath() as a base creates portable, relative paths.
---
Project Anti-Patterns
12. Calling clear() Without Safeguards
# WRONG: Destroys everything without warning
QgsProject.instance().clear()
# CORRECT: Check for unsaved changes first (in GUI context)
project = QgsProject.instance()
if project.isDirty():
# Prompt user or save first
project.write()
project.clear()WHY: clear() immediately removes ALL layers, settings, and project state with no confirmation and no undo. In a plugin context, this destroys the user's work without warning.
---
CRS Anti-Patterns
13. Creating QgsCoordinateTransform Without Context
# WRONG: Missing transform context -- ignores datum transformation preferences
xform = QgsCoordinateTransform(source_crs, dest_crs)
# CORRECT: ALWAYS provide the project's transform context
xform = QgsCoordinateTransform(
source_crs,
dest_crs,
QgsProject.instance().transformContext()
)WHY: Without a QgsCoordinateTransformContext, the transform engine cannot apply datum shift grids or user-configured transformation preferences. This causes silent precision loss of up to several meters depending on the datums involved.
---
Provider Anti-Patterns
14. Exposing Auth Credentials in Logs
# WRONG: expandAuthConfig=True exposes passwords in plain text
uri = layer.dataProvider().uri()
print(f"Connection: {uri.uri(True)}") # Prints password!
# CORRECT: ALWAYS use False when displaying or logging URIs
uri = layer.dataProvider().uri()
print(f"Connection: {uri.uri(False)}") # Password maskedWHY: uri.uri(True) expands the authentication configuration and includes the raw username/password in the returned string. This leaks credentials to logs, console output, and error messages.
---
15. Assuming GeoPackage Layer Name
# WRONG: Assuming the first layer without checking
layer = QgsVectorLayer("data/multi.gpkg", "Data", "ogr")
# CORRECT: Specify the layer name explicitly
layer = QgsVectorLayer("data/multi.gpkg|layername=buildings", "Buildings", "ogr")
# OR: Enumerate sublayers first
temp = QgsVectorLayer("data/multi.gpkg", "temp", "ogr")
for sub in temp.dataProvider().subLayers():
name = sub.split(QgsDataProvider.SUBLAYER_SEPARATOR)[1]
print(f"Available layer: {name}")WHY: A GeoPackage file can contain multiple layers. Without |layername=, QGIS loads the first layer by internal order, which may not be the intended one. This causes subtle data errors when the wrong layer is loaded silently.
---
QGIS 4.0 Anti-Patterns
16. Using QVariant.Type for Field Definitions
# WRONG for QGIS 4.x: QVariant.Type is removed in Qt6
from qgis.PyQt.QtCore import QVariant
field = QgsField("name", QVariant.String)
# CORRECT for QGIS 4.x: Use QMetaType.Type
from qgis.PyQt.QtCore import QMetaType
field = QgsField("name", QMetaType.Type.QString)
# FORWARD-COMPATIBLE: Try QMetaType first, fall back to QVariant
try:
from qgis.PyQt.QtCore import QMetaType
field = QgsField("name", QMetaType.Type.QString)
except ImportError:
from qgis.PyQt.QtCore import QVariant
field = QgsField("name", QVariant.String)WHY: Qt6 removes QVariant.Type entirely. Code using QVariant.String, QVariant.Int, etc. will raise AttributeError in QGIS 4.x. The QMetaType.Type enum is the Qt6 replacement.
Working Code Examples (QGIS 3.44+ / PyQGIS 3.x)
Example 1: Standalone Script -- Load and Query a Vector Layer
from qgis.core import (
QgsApplication,
QgsVectorLayer,
QgsProject,
QgsFeatureRequest,
)
# Initialize QGIS (headless)
QgsApplication.setPrefixPath("/usr/share/qgis", True)
qgs = QgsApplication([], False)
qgs.initQgis()
# Load a GeoPackage layer
layer = QgsVectorLayer(
"data/buildings.gpkg|layername=buildings",
"Buildings",
"ogr"
)
if not layer.isValid():
raise RuntimeError("Layer failed to load")
QgsProject.instance().addMapLayer(layer)
# Query features with an expression filter
request = QgsFeatureRequest().setFilterExpression('"area_sqm" > 500')
for feature in layer.getFeatures(request):
print(f"Building {feature['name']}: {feature['area_sqm']} sqm")
print(f"Total features: {layer.featureCount()}")
print(f"CRS: {layer.crs().authid()}")
# ALWAYS clean up
qgs.exitQgis()---
Example 2: Plugin Entry Point with Full Lifecycle
from qgis.PyQt.QtWidgets import QAction, QMessageBox
from qgis.PyQt.QtGui import QIcon
from qgis.core import QgsProject
class AreaCalculatorPlugin:
def __init__(self, iface):
self.iface = iface
def initGui(self):
self.action = QAction(
QIcon(":/plugins/area_calc/icon.png"),
"Calculate Areas",
self.iface.mainWindow()
)
self.action.triggered.connect(self.run)
self.iface.addToolBarIcon(self.action)
self.iface.addPluginToMenu("Area Calculator", self.action)
def unload(self):
# ALWAYS clean up all registered GUI elements
self.iface.removeToolBarIcon(self.action)
self.iface.removePluginFromMenu("Area Calculator", self.action)
def run(self):
layer = self.iface.activeLayer()
if layer is None:
QMessageBox.warning(
self.iface.mainWindow(),
"No Layer",
"Select a vector layer first."
)
return
if not layer.isValid():
QMessageBox.critical(
self.iface.mainWindow(),
"Invalid Layer",
"The selected layer is not valid."
)
return
total_area = 0.0
for feature in layer.getFeatures():
geom = feature.geometry()
if geom and not geom.isNull():
total_area += geom.area()
QMessageBox.information(
self.iface.mainWindow(),
"Result",
f"Total area: {total_area:.2f} map units squared"
)
def classFactory(iface):
return AreaCalculatorPlugin(iface)---
Example 3: Project Operations -- Load, Modify, Save
from qgis.core import (
QgsApplication,
QgsProject,
QgsVectorLayer,
Qgis,
)
QgsApplication.setPrefixPath("/usr/share/qgis", True)
qgs = QgsApplication([], False)
qgs.initQgis()
project = QgsProject.instance()
# Load project with read flags (skip layer resolution for fast access)
readflags = Qgis.ProjectReadFlags()
readflags |= Qgis.ProjectReadFlag.DontResolveLayers
project.read("/projects/analysis.qgz", readflags)
print(f"Project CRS: {project.crs().authid()}")
print(f"Project home: {project.homePath()}")
print(f"Layers (unresolved): {len(project.mapLayers())}")
# Clear and load fresh
project.clear()
# Full load with layer resolution
project.read("/projects/analysis.qgz")
print(f"Layers (resolved): {len(project.mapLayers())}")
# Add a new layer
new_layer = QgsVectorLayer(
"Point?crs=EPSG:4326&field=name:string(50)&field=value:double",
"Analysis Points",
"memory"
)
if new_layer.isValid():
project.addMapLayer(new_layer)
# Save as new project
project.write("/projects/analysis_updated.qgz")
qgs.exitQgis()---
Example 4: Layer Tree Organization
from qgis.core import (
QgsProject,
QgsVectorLayer,
QgsRasterLayer,
QgsLayerTreeGroup,
QgsLayerTreeLayer,
)
project = QgsProject.instance()
root = project.layerTreeRoot()
# Create groups
basemap_group = root.addGroup("Basemaps")
analysis_group = root.addGroup("Analysis")
# Add raster to basemap group
raster = QgsRasterLayer("data/ortho.tif", "Orthophoto", "gdal")
if raster.isValid():
project.addMapLayer(raster, False) # False = don't add to root
basemap_group.addLayer(raster)
# Add vector to analysis group
vector = QgsVectorLayer("data/parcels.gpkg|layername=parcels", "Parcels", "ogr")
if vector.isValid():
project.addMapLayer(vector, False)
analysis_group.addLayer(vector)
# Traverse the layer tree
def print_tree(group, indent=0):
for child in group.children():
prefix = " " * indent
if isinstance(child, QgsLayerTreeGroup):
print(f"{prefix}Group: {child.name()}")
print_tree(child, indent + 1)
elif isinstance(child, QgsLayerTreeLayer):
layer = child.layer()
valid = "valid" if layer and layer.isValid() else "INVALID"
print(f"{prefix}Layer: {child.name()} ({valid})")
print_tree(root)---
Example 5: Background Processing with QgsTask
from qgis.core import QgsTask, QgsApplication, QgsProject, QgsVectorLayer
class BufferTask(QgsTask):
"""Creates buffers around features in a background thread."""
def __init__(self, layer_id, buffer_distance):
super().__init__("Buffer Analysis", QgsTask.CanCancel)
self.layer_id = layer_id
self.buffer_distance = buffer_distance
self.result_features = []
self.error_message = None
def run(self):
"""Runs on BACKGROUND thread -- NO GUI access, NO project writes."""
layer = QgsProject.instance().mapLayer(self.layer_id)
if not layer:
self.error_message = f"Layer {self.layer_id} not found"
return False
total = layer.featureCount()
for i, feature in enumerate(layer.getFeatures()):
if self.isCanceled():
return False
geom = feature.geometry()
if geom and not geom.isNull():
buffered = geom.buffer(self.buffer_distance, 8)
self.result_features.append((feature.id(), buffered))
self.setProgress((i + 1) / total * 100)
return True
def finished(self, result):
"""Runs on MAIN thread -- safe to update GUI and project."""
if result and self.result_features:
# Create result layer on main thread
result_layer = QgsVectorLayer(
"Polygon?crs=EPSG:28992",
"Buffer Results",
"memory"
)
# Add features to result layer...
QgsProject.instance().addMapLayer(result_layer)
elif self.error_message:
print(f"Task failed: {self.error_message}")
# Usage (inside QGIS context)
layer = iface.activeLayer()
if layer and layer.isValid():
task = BufferTask(layer.id(), 100.0)
QgsApplication.taskManager().addTask(task)---
Example 6: Signal/Slot Pattern for Layer Monitoring
from qgis.core import QgsProject, QgsMapLayer
def on_layers_added(layers):
"""Called when layers are added to the project."""
for layer in layers:
print(f"Layer added: {layer.name()} ({layer.providerType()})")
if not layer.isValid():
print(f" WARNING: Layer {layer.name()} is invalid!")
def on_layers_removed(layer_ids):
"""Called when layers are removed from the project."""
for layer_id in layer_ids:
print(f"Layer removed: {layer_id}")
# Connect signals
project = QgsProject.instance()
project.layersAdded.connect(on_layers_added)
project.layersRemoved.connect(on_layers_removed)
# Later, disconnect when no longer needed
project.layersAdded.disconnect(on_layers_added)
project.layersRemoved.disconnect(on_layers_removed)---
Example 7: Provider Registry Inspection
from qgis.core import QgsProviderRegistry
registry = QgsProviderRegistry.instance()
# List all available providers
print("Available providers:")
for provider_name in sorted(registry.providerList()):
metadata = registry.providerMetadata(provider_name)
if metadata:
print(f" {provider_name}: {metadata.description()}")
else:
print(f" {provider_name}")---
Example 8: Path Resolution for Cross-Platform Projects
import os
from qgis.core import QgsProject, QgsPathResolver
# Rewrite paths when loading a project from a different machine
def rewrite_paths(path):
replacements = {
"C:/Users/OldUser/Projects": "/home/newuser/projects",
"\\\\server\\share": "/mnt/share",
}
for old, new in replacements.items():
if path.startswith(old):
return path.replace(old, new, 1)
return path
preprocessor_id = QgsPathResolver.setPathPreprocessor(rewrite_paths)
# Load the project -- paths are rewritten during load
QgsProject.instance().read("/home/newuser/projects/analysis.qgz")
# Remove the preprocessor after loading
QgsPathResolver.removePathPreprocessor(preprocessor_id)
# Use os.path for constructing new paths
project_home = QgsProject.instance().homePath()
output_dir = os.path.join(project_home, "output")
os.makedirs(output_dir, exist_ok=True)---
Example 9: Forward-Compatible Field Creation (QGIS 3.x + 4.x)
from qgis.core import QgsField, QgsVectorLayer
# QGIS 3.x approach (works now, breaks in 4.x)
from qgis.PyQt.QtCore import QVariant
field_3x = QgsField("name", QVariant.String, len=100)
# Forward-compatible approach for QGIS 4.x (Qt6)
# Use this when targeting QGIS 3.40+ for future-proofing
try:
from qgis.PyQt.QtCore import QMetaType
field_4x = QgsField("name", QMetaType.Type.QString)
except ImportError:
# Fallback for older QGIS versions
from qgis.PyQt.QtCore import QVariant
field_4x = QgsField("name", QVariant.String)API Signatures Reference (QGIS 3.44+ / PyQGIS 3.x)
QgsApplication
The application singleton. Manages initialization, provider loading, and global settings.
class QgsApplication(QApplication):
# Static methods -- call BEFORE constructing QgsApplication
@staticmethod
def setPrefixPath(path: str, useDefaultPaths: bool = True) -> None
# Sets the QGIS installation prefix path.
# MUST be called before initQgis().
# useDefaultPaths: if True, default plugin/data paths are derived from prefix.
@staticmethod
def prefixPath() -> str
# Returns the current prefix path.
@staticmethod
def pluginPath() -> str
# Returns the path to installed plugins.
@staticmethod
def pkgDataPath() -> str
# Returns the path to the package data directory.
@staticmethod
def qgisSettingsDirPath() -> str
# Returns the path to the QGIS settings directory.
@staticmethod
def qgisUserDatabaseFilePath() -> str
# Returns the path to the user CRS database.
# Constructor
def __init__(self, argv: list, GUIenabled: bool) -> None
# argv: command line arguments (pass [] for scripts)
# GUIenabled: False for headless/server, True for GUI applications
# Instance methods
def initQgis(self) -> None
# Loads data providers, initializes auth system, sets up CRS database.
# MUST be called after constructor.
def exitQgis(self) -> None
# Removes provider and layer registries from memory.
# ALWAYS call this to prevent memory leaks in standalone scripts.
@staticmethod
def taskManager() -> QgsTaskManager
# Returns the application's task manager for background processing.
@staticmethod
def processingRegistry() -> QgsProcessingRegistry
# Returns the processing algorithm registry.
@staticmethod
def instance() -> QgsApplication
# Returns the singleton application instance.
@staticmethod
def authManager() -> QgsAuthManager
# Returns the authentication manager.---
QgsProject
The project singleton. Manages layers, settings, CRS, and project I/O.
class QgsProject(QObject):
@staticmethod
def instance() -> QgsProject
# Returns the singleton project instance.
# NOT thread-safe for write operations.
# Project I/O
def read(self, filename: str, flags: Qgis.ProjectReadFlags = Qgis.ProjectReadFlags()) -> bool
# Reads a project file (.qgs or .qgz). Returns True on success.
def write(self, filename: str = None) -> bool
# Writes the project. If filename is None, saves to current location.
# Returns True on success.
def clear(self) -> None
# Removes all layers, settings, and resets the project.
# WARNING: destroys everything immediately.
def fileName(self) -> str
# Returns the project file path.
def absoluteFilePath(self) -> str
# Returns the absolute path to the project file.
def homePath(self) -> str
# Returns the project home directory (directory containing the project file).
# Layer management
def addMapLayer(self, layer: QgsMapLayer, addToLegend: bool = True) -> QgsMapLayer
# Adds a layer to the project.
# addToLegend: if False, layer is not shown in layer tree.
# Returns the added layer (or None on failure).
def addMapLayers(self, layers: list[QgsMapLayer], addToLegend: bool = True) -> list[QgsMapLayer]
# Adds multiple layers at once.
def removeMapLayer(self, layerId: str) -> None
# Removes a layer by its unique ID string.
def removeMapLayers(self, layerIds: list[str]) -> None
# Removes multiple layers by ID.
def removeAllMapLayers(self) -> None
# Removes all layers from the project.
def mapLayers(self) -> dict[str, QgsMapLayer]
# Returns all layers as {layerId: layer} dict.
def mapLayersByName(self, name: str) -> list[QgsMapLayer]
# Returns layers matching the given name.
# Names are NOT unique -- ALWAYS handle list results.
def mapLayer(self, layerId: str) -> QgsMapLayer
# Returns a single layer by its unique ID, or None.
def layerTreeRoot(self) -> QgsLayerTree
# Returns the root of the layer tree (Table of Contents).
# CRS and transforms
def crs(self) -> QgsCoordinateReferenceSystem
# Returns the project CRS.
def setCrs(self, crs: QgsCoordinateReferenceSystem) -> None
# Sets the project CRS.
def transformContext(self) -> QgsCoordinateTransformContext
# Returns the project's coordinate transform context.
# ALWAYS pass this to QgsCoordinateTransform constructors.
def ellipsoid(self) -> str
# Returns the project ellipsoid (e.g., "WGS84").
# Signals
layersAdded = pyqtSignal(list) # Emitted when layers are added
layersRemoved = pyqtSignal(list) # Emitted when layers are removed (list of IDs)
layerWasAdded = pyqtSignal(QgsMapLayer) # Emitted for each individual layer added
cleared = pyqtSignal() # Emitted when the project is cleared
readComplete = pyqtSignal() # Emitted after project read completes
writeComplete = pyqtSignal() # Emitted after project write completes---
QgsMapLayer
Abstract base class for all layer types.
class QgsMapLayer(QObject):
def id(self) -> str
# Returns the unique layer ID (auto-generated, stable within session).
def name(self) -> str
# Returns the display name.
def setName(self, name: str) -> None
# Sets the display name.
def type(self) -> Qgis.LayerType
# Returns the layer type enum (VectorLayer, RasterLayer, etc.).
def isValid(self) -> bool
# Returns True if the layer loaded successfully.
# ALWAYS check this after creation.
def crs(self) -> QgsCoordinateReferenceSystem
# Returns the layer's CRS.
def setCrs(self, crs: QgsCoordinateReferenceSystem) -> None
# Sets the layer's CRS.
def extent(self) -> QgsRectangle
# Returns the layer's spatial extent.
def source(self) -> str
# Returns the data source URI string.
def providerType(self) -> str
# Returns the provider name (e.g., "ogr", "gdal", "postgres").
def dataProvider(self) -> QgsDataProvider
# Returns the data provider instance.
def setCustomProperty(self, key: str, value) -> None
# Stores a custom key-value property on the layer.
def customProperty(self, key: str, defaultValue=None) -> Any
# Retrieves a custom property.
def clone(self) -> QgsMapLayer
# Returns a deep copy of the layer.---
QgsVectorLayer
Vector layer with features (points, lines, polygons) and attributes.
class QgsVectorLayer(QgsMapLayer):
def __init__(self, uri: str, baseName: str, providerLib: str) -> None
# uri: data source URI (provider-specific format)
# baseName: display name in layer tree
# providerLib: provider identifier ("ogr", "postgres", "memory", etc.)
def featureCount(self) -> int
# Returns the number of features.
def fields(self) -> QgsFields
# Returns the field (attribute) schema.
def geometryType(self) -> Qgis.GeometryType
# Returns Point, Line, Polygon, UnknownGeometry, or NullGeometry.
def wkbType(self) -> Qgis.WkbType
# Returns the WKB geometry type (more specific than geometryType).
def getFeatures(self, request: QgsFeatureRequest = QgsFeatureRequest()) -> QgsFeatureIterator
# Returns an iterator over features matching the request.
def getFeature(self, fid: int) -> QgsFeature
# Returns a single feature by feature ID.
def startEditing(self) -> bool
# Starts an edit session on the layer.
def commitChanges(self) -> bool
# Commits pending changes to the data provider.
def rollBack(self) -> bool
# Rolls back pending changes.
def isEditable(self) -> bool
# Returns True if the layer is in edit mode.
def updateExtents(self) -> None
# Recalculates the layer extent.
# ALWAYS call after adding features to a memory layer.
def updateFields(self) -> None
# Refreshes the field list from the data provider.
# ALWAYS call after modifying field structure via dataProvider().addAttributes().
def selectByExpression(self, expression: str) -> None
# Selects features matching the expression.
def selectedFeatures(self) -> list[QgsFeature]
# Returns currently selected features.
def selectedFeatureCount(self) -> int
# Returns the number of selected features.
def renderer(self) -> QgsFeatureRenderer
# Returns the layer's renderer (symbology).
def setRenderer(self, renderer: QgsFeatureRenderer) -> None
# Sets the layer's renderer.---
QgsRasterLayer
Raster layer for grid-based data.
class QgsRasterLayer(QgsMapLayer):
def __init__(self, uri: str, baseName: str, providerType: str = "gdal") -> None
# uri: file path or provider-specific URI
# baseName: display name
# providerType: provider identifier ("gdal", "wms", "wcs")
def width(self) -> int
# Returns raster width in pixels.
def height(self) -> int
# Returns raster height in pixels.
def bandCount(self) -> int
# Returns the number of raster bands.
def bandName(self, bandNo: int) -> str
# Returns the name of a specific band (1-based index).
def rasterUnitsPerPixelX(self) -> float
# Returns the horizontal resolution.
def rasterUnitsPerPixelY(self) -> float
# Returns the vertical resolution.
def renderer(self) -> QgsRasterRenderer
# Returns the raster renderer.
def setRenderer(self, renderer: QgsRasterRenderer) -> None
# Sets the raster renderer.---
QgsProviderRegistry
Singleton registry for all data providers.
class QgsProviderRegistry:
@staticmethod
def instance() -> QgsProviderRegistry
# Returns the singleton provider registry.
def providerList(self) -> list[str]
# Returns a list of all registered provider names.
# Example: ["ogr", "gdal", "postgres", "memory", "wms", "wfs", ...]
def providerMetadata(self, providerKey: str) -> QgsProviderMetadata
# Returns metadata for a specific provider.
def providerCapabilities(self, providerKey: str) -> int
# Returns capability flags for a provider.
def createProvider(self, providerKey: str, dataSource: str) -> QgsDataProvider
# Creates a new data provider instance.
def library(self, providerKey: str) -> str
# Returns the library path for a provider.---
QgsPathResolver
Static methods for path preprocessing during project load/save.
class QgsPathResolver:
@staticmethod
def setPathPreprocessor(preprocessor: Callable[[str], str]) -> str
# Registers a function to rewrite paths during project loading.
# Returns an ID for later removal.
@staticmethod
def setPathWriter(writer: Callable[[str], str]) -> str
# Registers a function to rewrite paths during project saving.
# Returns an ID for later removal.
@staticmethod
def removePathPreprocessor(id: str) -> None
# Removes a previously registered path preprocessor.
@staticmethod
def removePathWriter(id: str) -> None
# Removes a previously registered path writer.---
QgsTask
Base class for background tasks (subclass of QRunnable).
class QgsTask(QObject):
CanCancel = ... # Flag indicating the task can be cancelled
def __init__(self, description: str, flags: QgsTask.Flags = QgsTask.Flags()) -> None
def run(self) -> bool
# Override this. Runs on a BACKGROUND thread.
# NEVER access GUI or QgsProject.instance() for writes here.
# Return True for success, False for failure.
def finished(self, result: bool) -> None
# Override this. Runs on the MAIN thread after run() completes.
# Safe to update GUI and project here.
def isCanceled(self) -> bool
# Check if cancellation was requested.
def setProgress(self, progress: float) -> None
# Report progress (0-100).