PyQGIS Script Crashes with a Segmentation Fault: How to Fix It
Problem statement
The script produces its output, prints "done", and then dies:
wrote data/out/buffered.gpkg
Segmentation fault (core dumped)
$ echo $?
139
Or it crashes halfway through with no message at all, and in a container the exit code is simply 139 (128 + SIGSEGV). There is no Python traceback, because the crash happened in C++ β Qt or GDAL or GEOS β and the interpreter never got a chance to report anything.
A segfault in PyQGIS is nearly always a lifetime problem: a C++ object was used after the thing that owned it was destroyed, or the application was torn down in the wrong order.
Common causes:
QgsApplication.exitQgis()is never called, so cleanup happens at interpreter shutdown in an undefined orderexitQgis()is called while layers or aQgsProjectstill hold references- the
QgsApplicationobject is a local variable that goes out of scope while still in use - a layer created inside a function is garbage-collected while a Processing algorithm still uses it
- PyQGIS is used from a thread or a forked worker process
- the QGIS Python bindings do not match the installed QGIS build
- a plugin or a mismatched GDAL is loaded into the same process
Quick answer
To stop a PyQGIS script from segfaulting:
- keep the
QgsApplicationin a module-level variable for the whole run - initialise once, in the documented order, and tear down once in a
finally - call
QgsProject.instance().clear()and drop layer references beforeexitQgis() - keep a Python reference to every layer for as long as it is used
- never fork, thread, or reuse a QGIS process β run one task per process
import sys
from qgis.core import QgsApplication, QgsVectorLayer, QgsProject
QgsApplication.setPrefixPath("/usr", True)
qgs = QgsApplication([], False) # module level: alive for the whole run
qgs.initQgis()
try:
layer = QgsVectorLayer("data/raw/parcels.gpkg", "parcels", "ogr")
if not layer.isValid():
raise SystemExit("layer failed to load")
QgsProject.instance().addMapLayer(layer)
print(layer.featureCount(), "features")
finally:
QgsProject.instance().clear() # release layers first
qgs.exitQgis() # then tear down the application
The order in the finally block is the fix in most cases: release what the application owns, then shut the application down.
What a segfault actually means here
Step-by-step solution
Confirm it is a segfault, not an exception
python run.py; echo "exit=$?"
Exit code 139 is SIGSEGV (128 + 11); 134 is SIGABRT, usually an assertion inside Qt. Both mean the process was killed by a signal, so no except block ran and no atexit handler fired. On Linux you can get a stack trace:
ulimit -c unlimited
gdb -q -batch -ex run -ex bt --args python run.py 2>&1 | tail -40
Even a rough backtrace tells you whether the crash is in Qt, GEOS or GDAL, which narrows the search enormously.
Hold the application object for the whole run
A very common shape puts initialisation in a function. When the function returns, the local qgs is collected, the C++ QgsApplication is destroyed, and everything created from it becomes a dangling pointer.
# crashes: qgs dies when init() returns
def init():
qgs = QgsApplication([], False)
qgs.initQgis()
init()
layer = QgsVectorLayer(...) # boom, sooner or later
# correct: module-level, alive for the process
QgsApplication.setPrefixPath("/usr", True)
QGS = QgsApplication([], False)
QGS.initQgis()
If you prefer a function, return the object and keep it bound at module scope, or use a context manager (see the examples below).
Initialise in the documented order
import os, sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") # before any Qt import
sys.path.append("/usr/share/qgis/python/plugins") # for the processing package
from qgis.core import QgsApplication
QgsApplication.setPrefixPath("/usr", True) # before construction
QGS = QgsApplication([], False) # False = no GUI
QGS.initQgis() # providers, CRS database
from processing.core.Processing import Processing # import after initQgis
import processing
Processing.initialize() # registers native: algorithms
Importing processing before initQgis() is a classic crash-at-first-run: the algorithm registry is populated against an application that does not exist yet.
Tear down in the reverse order
finally:
QgsProject.instance().clear() # drops layers held by the project
del layer # drop your own references
QGS.exitQgis()
Skipping exitQgis() is not harmless. Without it, the C++ objects are destroyed during interpreter shutdown, in whatever order Python's garbage collector happens to pick β which is precisely the situation that produces a crash after the script's last line.
Keep Python references alive
The Python objects are thin wrappers, and when the wrapper is collected the underlying object may go with it. Anything still in use needs a reference.
# risky: the layer has no owner after the function returns
def load(path):
return QgsVectorLayer(path, "tmp", "ogr").featureCount()
# safe: the project owns it, or you keep the name bound
layer = QgsVectorLayer(path, "parcels", "ogr")
QgsProject.instance().addMapLayer(layer) # project takes ownership
The same applies to QgsProcessingContext, QgsCoordinateTransformContext and feedback objects passed into processing.run() β bind them to a name that outlives the call.
Do not thread or fork
Qt object trees are bound to the thread that created them, and fork() copies a process without copying its threads or native locks. Both produce crashes that look random.
# do not do this
from concurrent.futures import ThreadPoolExecutor
ThreadPoolExecutor().map(run_qgis_task, files)
# do this: one QGIS process per task, driven from an ordinary Python parent
import subprocess
for path in files:
subprocess.run([sys.executable, "qgis_task.py", str(path)], check=True)
A subprocess per file also isolates crashes: file 47 taking down its own process costs you one file, not the run. qgis_process β the CLI shipped with QGIS β is a ready-made version of the same idea.
Rule out a version mismatch
Python bindings compiled against a different QGIS build will crash in ways no code change fixes.
from qgis.core import Qgis
print("QGIS", Qgis.QGIS_VERSION)
import qgis, sys
print("bindings:", qgis.__file__)
print("python :", sys.executable)
qgis_process --version # what the installed QGIS reports
If the bindings come from a pip package or a conda environment while qgis_process reports a system install, that mismatch is the bug. Use the interpreter QGIS ships with, or install qgis from the same channel as everything else.
Code examples
Example 1: a context manager that always tears down correctly
"""qgis_session.py β one place that knows the lifecycle."""
import os, sys
from contextlib import contextmanager
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from qgis.core import QgsApplication, QgsProject
@contextmanager
def qgis_session(prefix="/usr", plugins="/usr/share/qgis/python/plugins"):
QgsApplication.setPrefixPath(prefix, True)
app = QgsApplication([], False)
app.initQgis()
if plugins not in sys.path:
sys.path.append(plugins)
from processing.core.Processing import Processing
Processing.initialize()
try:
yield app
finally:
QgsProject.instance().clear()
app.exitQgis()
from qgis_session import qgis_session
import processing
with qgis_session():
result = processing.run("native:buffer", {
"INPUT": "data/raw/parcels.gpkg",
"DISTANCE": 50,
"OUTPUT": "data/out/buffered.gpkg",
})
print(result["OUTPUT"])
The finally runs even when the body raises, so a failing algorithm no longer leaves a half-torn-down application behind.
Example 2: one process per file, crashes contained
# driver.py β an ordinary Python script, no QGIS imports at all
import subprocess, sys
from pathlib import Path
files = sorted(Path("data/raw").glob("*.gpkg"))
ok, failed = [], []
for path in files:
proc = subprocess.run(
[sys.executable, "qgis_task.py", str(path)],
capture_output=True, text=True, timeout=900,
)
if proc.returncode == 0:
ok.append(path.name)
else:
failed.append((path.name, proc.returncode, proc.stderr.strip()[-400:]))
print(f"{len(ok)} ok, {len(failed)} failed")
for name, code, err in failed:
marker = " (segfault)" if code in (139, -11) else ""
print(f" ! {name}: exit {code}{marker}\n {err}")
Checking for 139 explicitly turns "it crashed" into a diagnosis you can act on.
Example 3: narrowing down where the crash happens
import faulthandler, sys
faulthandler.enable(file=sys.stderr, all_threads=True)
faulthandler installs a signal handler that prints the Python stack when the process receives SIGSEGV. It cannot tell you which C++ line failed, but it names the Python call that was in flight β usually enough to identify the object with the broken lifetime.
Example 4: a minimal reproducer to bisect against
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from qgis.core import QgsApplication, QgsVectorLayer, QgsProject
QgsApplication.setPrefixPath("/usr", True)
app = QgsApplication([], False)
app.initQgis()
layer = QgsVectorLayer("data/raw/parcels.gpkg", "p", "ogr")
print("valid:", layer.isValid(), "features:", layer.featureCount())
QgsProject.instance().clear()
app.exitQgis()
print("clean exit")
If this crashes, the problem is the environment β bindings, prefix path, or a library conflict β not your pipeline. If it exits cleanly, add your steps back one at a time.
Explanation
PyQGIS is a set of SIP bindings over a large C++ library. Each Python object holds a pointer to a C++ object, and ownership of that C++ object may sit on either side of the boundary. A segmentation fault is what happens when code follows a pointer to memory that has already been freed β so the question is never "what raised?" but "what was destroyed too early?".
QgsApplication sits at the root of that ownership graph. It owns the provider registry, the CRS database, the Processing registry and the style manager, and virtually every other object depends on at least one of them. Destroying it while layers are alive leaves those layers pointing at freed registries. Equally, letting the interpreter shut down without calling exitQgis() means the C++ destructors run during Python finalisation, in an order nobody controls β which is why so many PyQGIS scripts crash after printing their last line. The crash is the cleanup, not the work.
Reference counting is the second half of the story. A QgsVectorLayer created in Python is owned by Python until you hand it to something else, typically QgsProject.instance().addMapLayer(). If nothing holds a reference, the wrapper is collected and the C++ layer goes with it β even though a Processing algorithm may still be using it. That is why the guidance is so consistently "keep a name bound": the name is the ownership.
Threads and forks break the model in a different way. Qt requires that objects be used on the thread that created them, and fork() produces a child that has the parent's memory but none of its threads, leaving native locks held by threads that no longer exist. Neither situation raises; both crash. The practical consequence is that parallelism in QGIS automation is achieved with processes that each initialise QGIS themselves β which is exactly what qgis_process does, and why it is the most robust way to batch QGIS work.
Edge cases or notes
- Exit code 139 vs 134: 139 is SIGSEGV (bad memory access), 134 is SIGABRT (an assertion or an uncaught C++ exception). They usually have different causes.
- A crash only in Docker: Often a missing
QT_QPA_PLATFORM=offscreen, or a container without/dev/shmspace for Qt's shared memory. del layeris not enough: Only if nothing else holds a reference βQgsProject, a layer tree node, or a Processing context may still own it. CallQgsProject.instance().clear()first.- Do not mix conda GDAL with system QGIS: Two GDAL builds in one process is a reliable way to crash inside a driver.
- The Python Console has its own application: Code that works pasted into QGIS may crash standalone, because the console never runs the init/exit sequence you now own.
processing.runwithTEMPORARY_OUTPUT: The returned layer lives in the Processing context. Convert it or write it out before the context is destroyed.- Qt plugins loaded twice: Setting
QT_PLUGIN_PATHto a directory from a different Qt build causes crashes at start-up rather than at exit.
Internal links
- How to Run a PyQGIS Script Headless Without Opening QGIS
- How to Automate QGIS with Python (PyQGIS): The Complete Workflow
- How to Batch Process Layers in QGIS with PyQGIS
- How to Fix "ModuleNotFoundError: No module named 'qgis'"
- qt.qpa.plugin: Could Not Load the Qt Platform Plugin xcb in PyQGIS
- PyQGIS Layer Fails to Load (isValid() Returns False): How to Fix It
FAQ
Why does my script crash after it prints "done"?
Because the crash is in the cleanup. Without exitQgis(), the C++ objects are destroyed during interpreter shutdown in an arbitrary order. Clear the project and call exitQgis() in a finally block.
Do I really need exitQgis() if the script is about to end anyway?
Yes. It performs an ordered teardown of registries and providers. Leaving it out is the single most common cause of exit-time segfaults, and it also flushes provider caches.
Can I run PyQGIS in a thread pool?
No. Qt objects belong to the thread that created them, and the Processing framework is not thread-safe. Use one process per task, spawned with subprocess, or call qgis_process directly.
Why does the same code work in the QGIS Python Console?
The console runs inside a fully initialised application that it owns and never tears down mid-session. Standalone, that lifecycle is yours to manage β which is where the crashes come from.
How do I get any diagnostic information from a segfault?
Call faulthandler.enable() at the top of the script to print the Python stack on SIGSEGV, and run under gdb --args python script.py for a native backtrace. Exit code 139 confirms the signal.
Could a bad input file cause a segfault?
It can β a corrupt GeoPackage or a malformed geometry can crash a driver. Isolate it by running one file per process; a crash then costs you one file and names it precisely.
Is qgis_process safer than writing my own PyQGIS script?
For straightforward algorithm runs, yes. It initialises and tears down QGIS correctly for every invocation, and a crash is confined to that one run. Write PyQGIS when you need logic that the CLI cannot express.