Install
$ agentstack add skill-impertio-studio-qgis-claude-skill-package-qgis-core-architecture ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
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 → QgsAnnotationLayer
Which 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](references/methods.md) -- API signatures for QgsProject, QgsApplication, QgsMapLayer, QgsProviderRegistry
- [references/examples.md](references/examples.md) -- Working code examples verified against PyQGIS 3.44 documentation
- [references/anti-patterns.md](references/anti-patterns.md) -- What NOT to do, with WHY explanations
Official Sources
- https://docs.qgis.org/latest/en/docs/pyqgisdevelopercookbook/intro.html
- https://docs.qgis.org/latest/en/docs/pyqgisdevelopercookbook/loadproject.html
- https://docs.qgis.org/latest/en/docs/pyqgisdevelopercookbook/loadlayer.html
- https://qgis.org/pyqgis/3.44/
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Impertio-Studio
- Source: Impertio-Studio/QGIS-Claude-Skill-Package
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.