Running Python Scripts Outside QGIS Desktop
To run Python scripts outside the QGIS desktop, you must manually bootstrap the PyQGIS environment before importing any qgis modules. This requires setting QGIS_PREFIX_PATH, PATH, and PYTHONPATH to your QGIS installation directory, then calling QgsApplication.initQgis() in headless mode (False). This bypasses the GUI while retaining full access to vector/raster APIs, GDAL/OGR drivers, and the Processing framework.
For reproducible deployments, isolate these dependencies to prevent system-level conflicts. A properly configured Virtual Environments for GIS ensures your standalone interpreter resolves QGIS libraries consistently across development and production machines. The foundational mapping between Python interpreters and QGIS binaries is detailed in the PyQGIS Fundamentals & Environment Setup reference, which covers version alignment and dependency resolution.
Working Code Snippet
import os
import sys
# 1. Set QGIS installation root (adjust per OS)
QGIS_PREFIX = r"C:\OSGeo4W\apps\qgis-ltr" # Windows OSGeo4W
# macOS: "/Applications/QGIS.app/Contents/MacOS"
# Linux: "/usr"
# 2. Inject environment variables BEFORE importing qgis modules
os.environ["QGIS_PREFIX_PATH"] = QGIS_PREFIX
os.environ["PATH"] = os.pathsep.join([
os.path.join(QGIS_PREFIX, "bin"),
os.environ.get("PATH", ""),
])
# Prepend Python path so qgis.core resolves correctly
sys.path.insert(0, os.path.join(QGIS_PREFIX, "python"))
sys.path.insert(0, os.path.join(QGIS_PREFIX, "python", "plugins"))
from qgis.core import QgsApplication, QgsVectorLayer
# 3. Initialize headless QGIS application
qgs = QgsApplication([], False) # False disables GUI
qgs.setPrefixPath(QGIS_PREFIX, True)
qgs.initQgis()
# 4. Execute GIS logic
layer = QgsVectorLayer("data/roads.shp", "roads", "ogr")
if layer.isValid():
print(f"Loaded {layer.featureCount()} features")
else:
print("Layer failed to load. Check path and OGR drivers.")
# 5. Clean shutdown
qgs.exitQgis()
Compatibility Requirements
| Component | Requirement | Failure Symptom |
|---|---|---|
| Python Version | Must exactly match QGIS bundled version (e.g., 3.9 for QGIS 3.28, 3.12 for QGIS 3.34+) | ImportError: DLL load failed or ModuleNotFoundError: qgis |
| QGIS Major Version | Scripts must target the exact installed major.minor release | AttributeError on missing classes added in later releases |
| Path Separators | Use os.pathsep and os.path.join to avoid OS-specific crashes | FileNotFoundError during setPrefixPath() |
| macOS Bundle | Point QGIS_PREFIX_PATH to Contents/MacOS, not the .app root | QgsApplication initializes but fails to load providers |
| Linux Headless | Export QT_QPA_PLATFORM=offscreen before execution | qt.qpa.xcb: could not connect to display crash |
Execution & Integration
- Direct CLI:
python standalone_script.py - Scheduled Tasks/Cron: Wrap execution in a batch/shell script that exports environment variables first, then invokes Python.
- Processing Framework: To use
processing.run(), register native algorithms after initialization:
from qgis.analysis import QgsNativeAlgorithms
import processing
QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())
Troubleshooting & Fallbacks
If initialization fails or crashes silently, apply these diagnostic steps:
- Verify Path Resolution: Print
sys.executableandos.environ["QGIS_PREFIX_PATH"]immediately after assignment. A mismatched interpreter is the most common cause of missingqgismodules. - Use
qgis_process(Recommended for CI/CD): QGIS 3.14+ ships with a standalone CLI tool that auto-configures environments. Run:qgis_process run "native:buffer" -- INPUT=roads.shp DISTANCE=100 OUTPUT=buffered.gpkg
To run a custom Python script viaqgis_process, wrap it as a Processing algorithm. This is the most reliable fallback for CI/CD pipelines. - Conda Environment Fallback: If system paths are unstable or permissions restrict
PATHmodification, useconda-forge:conda create -n qgis-standalone -c conda-forge qgis python=3.12 conda activate qgis-standalone python standalone_script.py
Conda automatically resolves GDAL, PROJ, Qt, and SIP bindings. - Enable Debug Logging: Add
QgsApplication.messageLog().logMessage("Init check", "Standalone")immediately afterinitQgis()and setQGIS_LOG_FILE=/path/to/debug.login your environment to capture silent provider load failures. - GDAL Driver Registration: If formats fail to open, explicitly call
from osgeo import gdal; gdal.UseExceptions()and verifyQGIS_PREFIX_PATHincludes the directory containing GDAL plugins.
Standalone execution removes GUI overhead while preserving the full spatial engine. Always validate environment variables at runtime, match Python versions exactly, and prefer qgis_process or Conda when manual path mapping proves unstable.
Frequently Asked Questions
Why must I set the environment variables before importing any qgis module?
The qgis.core import triggers loading of compiled C++ libraries (Qt, GDAL, PROJ) that are resolved against PATH and QGIS_PREFIX_PATH at import time. If those variables are set after the import, the dynamic linker has already searched the wrong locations, producing ImportError: DLL load failed or silent provider failures.
What does the False argument in QgsApplication([], False) do?
The second argument controls GUI initialization. Passing False runs QGIS in headless mode, so no windows or display server are required. This is what makes the script suitable for cron jobs, servers, and CI/CD runners.
Why does my standalone script crash on a Linux server with no display?
Even in headless mode, Qt may try to connect to an X display. Export QT_QPA_PLATFORM=offscreen before running the script so Qt uses the offscreen rendering backend instead of attempting an X11 connection.
My layer loads with zero features but no error appears. What went wrong?
This usually means GDAL/OGR drivers were not registered because QGIS_PREFIX_PATH is incomplete or initQgis() was not called. Verify the prefix path, confirm initQgis() ran, and call gdal.UseExceptions() so driver errors surface instead of failing silently.
When should I use qgis_process instead of bootstrapping QgsApplication myself?
Use qgis_process for CI/CD and scheduled tasks where you mainly run existing Processing algorithms. It auto-configures the environment and avoids fragile manual path mapping. Bootstrap QgsApplication directly only when you need custom Python logic that goes beyond a single algorithm call.