Properly Cleaning Up Plugin Resources on QGIS Shutdown
To properly clean up plugin resources on QGIS shutdown, implement a deterministic unload() method that explicitly reverses every initialization step:…
To properly clean up plugin resources on QGIS shutdown, implement a deterministic unload() method that explicitly reverses every initialization step: disconnect all Qt signals, terminate background threads, close file and database handles, deregister custom map tools, and remove UI elements. QGIS automatically invokes unload() during application exit, but Python’s garbage collector will not reliably release C+±backed QGIS objects, locked files, or active thread pools. Manual teardown is mandatory to prevent memory leaks, corrupted project states, and OS-level file locks.
Why Explicit Teardown Is Non-Negotiable
The Plugin Lifecycle and Resource Management workflow dictates that cleanup must be ordered and explicit. When QGIS begins its shutdown sequence, it iterates through the plugin registry and calls unload() on each active module. If your plugin registers a QgsMapTool, opens a persistent sqlite3 connection, or starts a QThread for heavy geoprocessing, those resources persist in memory or on disk until explicitly released.
Python’s reference counting and cyclic garbage collector are unaware of QGIS’s underlying C++ object graph. Relying on __del__ or implicit scope exit frequently leaves dangling pointers, orphaned toolbar actions, and locked SQLite WAL files. The cleanup order matters: stop background workers first, disconnect signals, tear down UI components, and finally clear class-level references.
flowchart TD
A["unload() called"] --> B["1. Stop background threads: quit + wait"]
B --> C["2. Disconnect signals in try/except"]
C --> D["3. Deregister map tools and UI"]
D --> E["4. Close DB and file handles"]
E --> F["5. Clear Python references to None"]
Production-Ready unload() Template
The following implementation covers the most common leak vectors in QGIS 3.x plugins. It includes defensive error handling, explicit thread termination, and safe UI deregistration. For complete plugin scaffolding guidelines, refer to the official PyQGIS Developer Cookbook.
from qgis.core import QgsProject, QgsApplication, QgsMessageLog, Qgis
from qgis.gui import QgsMapTool
from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import QAction
import sqlite3
import os
class MyPlugin:
def __init__(self, iface):
self.iface = iface
self.actions = []
self.map_tool = None
self.worker_thread = None
self.db_conn = None
self._temp_files = []
def initGui(self):
# UI setup
self.action = QAction("Run Process", self.iface.mainWindow())
self.iface.addToolBarIcon(self.action)
self.actions.append(self.action)
self.action.triggered.connect(self.run_process)
# Map tool registration
self.map_tool = QgsMapTool(self.iface.mapCanvas())
self.iface.mapCanvas().setMapTool(self.map_tool)
# Background thread
self.worker_thread = QThread()
self.worker_thread.start()
# Database connection
self.db_conn = sqlite3.connect("plugin_cache.db")
# Track temp files for cleanup
self._temp_files.append("plugin_cache.db")
def unload(self):
"""Explicitly clean up all resources on QGIS shutdown."""
try:
# 1. Stop background threads gracefully
if self.worker_thread and self.worker_thread.isRunning():
self.worker_thread.quit()
# Block until thread finishes (max 3s to avoid hanging shutdown)
if not self.worker_thread.wait(3000):
QgsMessageLog.logMessage(
"Worker thread failed to terminate within timeout.",
"MyPlugin", Qgis.Critical
)
# 2. Disconnect signals
try:
QgsProject.instance().layerWasAdded.disconnect(self._handle_layer)
except (TypeError, RuntimeError):
pass # Already disconnected or never connected
# 3. Deregister map tool
if self.map_tool:
canvas = self.iface.mapCanvas()
if canvas.mapTool() == self.map_tool:
canvas.unsetMapTool(self.map_tool)
self.map_tool = None
# 4. Remove UI elements
for action in self.actions:
self.iface.removeToolBarIcon(action)
try:
action.triggered.disconnect(self.run_process)
except (TypeError, RuntimeError):
pass
self.actions.clear()
# 5. Close database & file handles
if self.db_conn:
try:
self.db_conn.close()
except sqlite3.Error:
pass
self.db_conn = None
# 6. Clean up tracked temporary files
for fpath in self._temp_files:
if os.path.exists(fpath):
try:
os.remove(fpath)
except OSError:
QgsMessageLog.logMessage(
f"Failed to remove temp file: {fpath}",
"MyPlugin", Qgis.Warning
)
self._temp_files.clear()
QgsMessageLog.logMessage("Plugin resources successfully released.", "MyPlugin")
except Exception as e:
QgsMessageLog.logMessage(f"Critical unload error: {e}", "MyPlugin", Qgis.Critical)
def run_process(self):
pass # Placeholder for plugin logic
def _handle_layer(self, layer):
pass # Placeholder for signal handler
Execution Order Breakdown
- Thread Termination: Call
quit()to exit the event loop, thenwait(timeout)to block until the thread finishes. Never force-terminate threads; it corrupts shared memory and leaves mutexes locked. See Qt QThread Documentation for safe termination patterns. - Signal Disconnection: Wrap
disconnect()calls intry/except. If the slot was never connected or already removed, PyQt raisesTypeErrororRuntimeError. Silent failure here prevents cascade errors during shutdown. - UI & Map Tool Deregistration: Always check
canvas.mapTool() == self.map_toolbefore callingunsetMapTool(). Removing toolbar icons without clearing theactionslist leaves dangling references that QGIS attempts to render during the final UI teardown. - Database & File Handles: Close connections explicitly. SQLite leaves
-waland-shmfiles if connections aren’t cleanly closed, which can corrupt the database on next launch. - Reference Clearing: Set instance attributes to
Noneafter cleanup. This breaks circular references and allows Python’s GC to sweep the plugin object immediately.
Common Pitfalls & Debugging
- Hanging Shutdown: If
unload()blocks indefinitely, QGIS will force-kill the process. Always usewait(timeout)for threads and avoid synchronous network calls during teardown. - Double Unload: Some QGIS versions call
unload()twice during plugin manager reloads. Guard againstNonechecks and use.clear()on lists to make the method idempotent. - Missing
__init__Cleanup: If your plugin modifiesQgsProjectproperties or registers customQgsProcessingAlgorithmproviders, revert those changes inunload(). The Plugin Development & UI Integration guidelines emphasize that any global state mutation must be fully reversible. - Logging During Exit:
QgsMessageLogremains functional during shutdown, but avoid heavy I/O. Use it only for critical teardown failures.
Final Checklist Before Deployment
- Every
connect()ininitGui()has a matchingdisconnect()in - All
QThreadinstances callquit()+ - UI elements are removed via
iface.removeToolBarIcon()and - Method is wrapped in
try/except
Properly cleaning up plugin resources on QGIS shutdown is a deterministic, ordered process. When implemented correctly, your plugin will exit cleanly, leave no orphaned processes, and maintain project integrity across sessions.