Reload a QGIS Plugin Without Restarting
Restarting QGIS to test a one-line change is the fastest way to stop enjoying plugin development. A reload takes a second, a restart takes thirty plus the time to reopen the project — and over a day of iteration the difference is hours. The reason reloading is not simply automatic is that Python caches imported modules in sys.modules: re-running your plugin's entry point picks up the old code unless something explicitly clears the cache, and the sub-modules your plugin imports are cached separately from the package itself.
This page is a focused recipe within Plugin Boilerplate & Structure. It covers why a naive reload silently keeps stale code, the Plugin Reloader route, doing it from the console, and the unload() discipline that makes any of it work.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A plugin under development in your profile's
python/pluginsdirectory. - An
unload()method that genuinely undoes whatinitGui()did — this is the part everything else depends on.
Install Plugin Reloader
The pragmatic answer for day-to-day work is the Plugin Reloader plugin, which handles the module cache correctly and binds a reload to a keyboard shortcut.
- Open
Plugins > Manage and Install Plugins. - Search for Plugin Reloader and install it.
- Open
Plugins > Plugin Reloader > Choose a plugin to be reloadedand pick yours. - Press F5, or click its toolbar button, after every edit.
Plugin Reloader calls your plugin's unload(), purges its modules from sys.modules, re-imports the package, and calls initGui() again. Everything that makes a reload go wrong is therefore in your unload() — the reloader does its half correctly.
Reload from the console
When you would rather not depend on another plugin, or need the reload inside a script, the same sequence is a handful of lines.
import sys
from qgis import utils
PLUGIN = "myplugin"
utils.unloadPlugin(PLUGIN)
for name in list(sys.modules):
if name == PLUGIN or name.startswith(PLUGIN + "."):
del sys.modules[name]
utils.loadPlugin(PLUGIN)
utils.startPlugin(PLUGIN)
Breakdown: unloadPlugin() calls your unload() and removes the plugin instance from the registry. The loop is the part a naive reload misses: iterating over list(sys.modules) takes a copy of the keys so the dictionary can be mutated while looping, and matching both the exact name and the myplugin. prefix clears the package and every sub-module. Testing startswith(PLUGIN + ".") rather than startswith(PLUGIN) avoids also deleting an unrelated myplugintools package. loadPlugin() re-imports and startPlugin() calls initGui().
Write an unload that actually undoes things
from qgis.core import QgsExpression, QgsProject
class MyPlugin:
def unload(self):
canvas = self.iface.mapCanvas()
if canvas.mapTool() is self.tool:
canvas.unsetMapTool(self.tool)
for signal, slot in self._connections:
try:
signal.disconnect(slot)
except TypeError:
pass
self._connections.clear()
try:
QgsExpression.unregisterFunction("myplugin_utm_zone")
except Exception:
pass
if self.dock is not None:
self.iface.removeDockWidget(self.dock)
self.dock.deleteLater()
self.dock = None
for action in self.actions:
self.iface.removePluginMenu("&My Plugin", action)
self.iface.removeToolBarIcon(action)
action.deleteLater()
self.actions.clear()
self.tool = None
Breakdown: Keeping self._connections as a list of (signal, slot) pairs recorded at connect time is what makes the disconnect loop complete — relying on memory is how one connection gets missed and every action starts running twice. try/except TypeError makes each disconnect safe when the connection has already gone, so unload() never half-fails. deleteLater() schedules Qt objects for destruction on the event loop rather than freeing them while a signal may still be in flight. Setting attributes back to None releases the last Python references, which is what allows the module purge to actually free the old code. See Connect to Layer and Project Signals in PyQGIS for the signal side and Create a Custom Map Tool in PyQGIS for the tool side.
Verify the reload worked
A reload that silently kept old code is worse than no reload, because you debug a change that is not running. A version marker settles it in one glance.
# in __init__.py
__build__ = "2026-08-01T09:14"
def classFactory(iface):
from qgis.core import QgsMessageLog, Qgis
QgsMessageLog.logMessage(f"myplugin loaded, build {__build__}", "myplugin", Qgis.Info)
from .plugin import MyPlugin
return MyPlugin(iface)
Breakdown: Logging at classFactory time means the message appears on every load and reload, so the Log Messages panel becomes a reload history. Bumping __build__ — or deriving it from the file's modification time — makes a stale reload obvious immediately. Importing MyPlugin inside classFactory rather than at module top level is worth doing anyway: it keeps the import out of the cache until the plugin is actually started, which makes the purge more reliable.
What a reload cannot recover
Some state survives a reload because it does not live in your plugin's modules at all. Knowing which is which saves a lot of confused debugging when a change appears not to take effect.
The two that catch people most often have simple remedies. A changed icon or .ui file needs its resource module rebuilding with pyrcc5 before the reload will see it — and once resources_rc.py has been imported, it is cached like any other module, so it must be purged too. A setting written to QSettings during an earlier run persists across reloads and even restarts, which is desirable in production and confusing in development; clear the plugin's own keys explicitly when testing first-run behaviour.
A Processing provider is the third case: it is registered with QGIS's provider registry rather than held by your plugin, so unload() must call QgsApplication.processingRegistry().removeProvider() or the reload will fail on a duplicate identifier. See Processing Provider Plugins for the registration lifecycle.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | Identical. qgis.utils.unloadPlugin / loadPlugin / startPlugin unchanged. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | qgis.utils.reloadPlugin() exists and performs the module purge for you. |
Plugin Reloader is maintained separately from QGIS and works across all current 3.x releases. On 3.40 and newer, qgis.utils.reloadPlugin("myplugin") is a one-line alternative to the console recipe.
Troubleshooting
- The reload runs but the change is not there. Only the package was re-imported. Delete every
sys.modulesentry starting with the plugin name plus a dot. - Everything runs twice after a reload. Signals from the previous instance are still connected. Record connections and disconnect them all in
unload(). - Duplicate toolbar buttons appear.
removeToolBarIcon()was not called for every action created. Function is already registered. A custom expression function was not unregistered. CallQgsExpression.unregisterFunction()inunload().- QGIS crashes on reload. A map tool was still installed when its Python object was released. Call
unsetMapTool()before dropping the reference. - The dock widget reappears twice.
removeDockWidget()was skipped, so the old widget is still parented to the main window.
Conclusion
A fast reload loop is worth setting up on day one, and it rests entirely on unload() being a true mirror of initGui(). Install Plugin Reloader for the keystroke, keep a recorded list of signal connections so none is missed, purge every sub-module rather than just the package, and log a build marker so you can always tell whether the code running is the code you just wrote.
Frequently Asked Questions
Why does my plugin still run old code after reloading?
Python caches every module separately in sys.modules. Re-importing the package returns the cached object without re-reading sub-modules, so a change in tools.py is invisible. Delete every entry whose name is the plugin package or starts with the package name plus a dot.
Do I need the Plugin Reloader plugin?
No, but it is the least friction. It calls unload(), purges the modules and re-runs initGui() behind a keyboard shortcut. The same sequence is a handful of lines in the Python Console if you prefer no extra dependency.
Why does everything happen twice after I reload?
The previous instance's signal connections were never removed, so both the old and new handlers fire. Keep a list of (signal, slot) pairs as you connect them and disconnect them all in unload().
How can I be sure the reload actually picked up my change?
Log a build marker from classFactory() and bump it as you edit. The Log Messages panel then shows a reload history, and a stale build string tells you the purge did not work.
Does reloading a plugin reload its dependencies? Only those inside the plugin package. A third-party library imported from site-packages stays cached, so changes to it need a full QGIS restart or an explicit purge of its own modules.
Can I automate the reload on file save?
Yes — Plugin Reloader offers a watch mode, and editors can be configured to trigger it. It is a considerable convenience once the unload() path is genuinely correct.