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.

Why re-importing the plugin is not enoughThe Python module cache holds the plugin package alongside each of its sub-modules as separate entries. Re-importing the package returns the cached object and does not re-read any sub-module, so a change in a tool or dialog file is not picked up. A correct reload removes every entry whose name starts with the plugin package.Each file is its own cache entry — clearing one is not clearing them allsys.modulesmypluginmyplugin.toolsmyplugin.dialogmyplugin.utilsre-import the package onlyreturns the cached objectedits to tools.py are invisibledelete every myplugin.* entrynext import re-reads from diskevery change picked up

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A plugin under development in your profile's python/plugins directory.
  • An unload() method that genuinely undoes what initGui() 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.

  1. Open Plugins > Manage and Install Plugins.
  2. Search for Plugin Reloader and install it.
  3. Open Plugins > Plugin Reloader > Choose a plugin to be reloaded and pick yours.
  4. 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

Everything initGui() creates, unload() must removeSix paired rows. A toolbar action is created with addToolBarIcon and removed with removeToolBarIcon. A menu entry is added and removed. A map tool is installed with setMapTool and released with unsetMapTool. Signal connections are made with connect and undone with disconnect. A dock widget is added and removed. A custom expression function is registered and unregistered.Every line in initGui() needs its mirror in unload()initGui()unload()iface.addToolBarIcon(action)iface.removeToolBarIcon(action)iface.addPluginToMenu(...)iface.removePluginMenu(...)canvas.setMapTool(self.tool)canvas.unsetMapTool(self.tool)signal.connect(handler)signal.disconnect(handler)iface.addDockWidget(...)iface.removeDockWidget(...)QgsExpression.registerFunction(f)QgsExpression.unregisterFunction(n)

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.

What a reload refreshes and what it does notA reload re-reads Python modules, so functions, classes and plugin instances are new. It does not refresh compiled Qt resource files, values already stored in QSettings, the C plus plus side of a dialog whose UI file changed, or a Processing provider registered with QGIS, each of which needs its own action.A reload is a Python reload — nothing morerefreshed✓ every .py file in the package✓ classes and their methods✓ the plugin instance itself✓ module-level constantsthis is the part that worksnot refreshed✗ compiled resources_rc.py✗ values already in QSettings✗ a changed .ui file's C++ side✗ a registered Processing providereach needs its own action

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 versionPythonNotes
3.28 LTR3.9Identical. qgis.utils.unloadPlugin / loadPlugin / startPlugin unchanged.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12qgis.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.modules entry 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. Call QgsExpression.unregisterFunction() in unload().
  • 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.