[{"data":1,"prerenderedAt":1513},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart":3},{"id":4,"title":5,"body":6,"description":1502,"extension":1503,"meta":1504,"navigation":325,"path":1509,"seo":1510,"stem":1511,"__hash__":1512},"docs\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002Findex.md","Reload a QGIS Plugin Without Restarting",{"type":7,"value":8,"toc":1489},"minimark",[9,13,27,40,193,198,226,230,233,263,278,282,285,445,492,496,624,942,984,988,991,1095,1116,1120,1123,1197,1216,1231,1235,1241,1310,1317,1321,1382,1386,1395,1399,1412,1424,1435,1445,1451,1460,1464,1485],[10,11,5],"h1",{"id":12},"reload-a-qgis-plugin-without-restarting",[14,15,16,17,21,22,26],"p",{},"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 ",[18,19,20],"code",{},"sys.modules",": re-running your plugin's entry point picks up the ",[23,24,25],"em",{},"old"," code unless something explicitly clears the cache, and the sub-modules your plugin imports are cached separately from the package itself.",[14,28,29,30,35,36,39],{},"This page is a focused recipe within ",[31,32,34],"a",{"href":33},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","Plugin Boilerplate & Structure",". It covers why a naive reload silently keeps stale code, the Plugin Reloader route, doing it from the console, and the ",[18,37,38],{},"unload()"," discipline that makes any of it work.",[14,41,42],{},[43,44,49,53,57,64,81,90,100,105,145,151,157,161,165,169,173,177,181,189],"svg",{"viewBox":45,"role":46,"ariaLabel":47,"xmlns":48},"0 0 760 268","img","The module cache: a plugin package and its sub-modules cached in sys.modules, where re-importing the package alone leaves the sub-modules stale","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[50,51,52],"title",{},"Why re-importing the plugin is not enough",[54,55,56],"desc",{},"The 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.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","268","#f6f3ea",[65,66,67],"defs",{},[68,69,76],"marker",{"id":70,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"reloadArrow","0 0 10 10","8","5","7","auto-start-reverse",[77,78],"path",{"d":79,"fill":80},"M0 0 L10 5 L0 10 z","#2f3b35",[82,83,89],"text",{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Each file is its own cache entry — clearing one is not clearing them all",[58,91],{"x":92,"y":93,"width":94,"height":95,"rx":96,"fill":97,"stroke":98,"style":99},"16","52","300","184","10","#fffdf7","#59645f","stroke-width:2.5",[82,101,20],{"x":102,"y":103,"style":104,"fill":87,"textAnchor":88},"166","76","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif",[106,107,109,118,122,127,131,134,138,141],"g",{"style":108},"font-size:11px;font-family:monospace",[58,110],{"x":111,"y":112,"width":113,"height":85,"rx":114,"fill":115,"fillOpacity":116,"stroke":115,"style":117},"40","90","252","4","#b45309",0.2,"stroke-width:1.8",[82,119,121],{"x":93,"y":120,"fill":80},"108","myplugin",[58,123],{"x":111,"y":124,"width":113,"height":85,"rx":114,"fill":125,"fillOpacity":126,"stroke":125,"style":117},"124","#b91c1c",0.18,[82,128,130],{"x":93,"y":129,"fill":80},"142","myplugin.tools",[58,132],{"x":111,"y":133,"width":113,"height":85,"rx":114,"fill":125,"fillOpacity":126,"stroke":125,"style":117},"158",[82,135,137],{"x":93,"y":136,"fill":80},"176","myplugin.dialog",[58,139],{"x":111,"y":140,"width":113,"height":85,"rx":114,"fill":125,"fillOpacity":126,"stroke":125,"style":117},"192",[82,142,144],{"x":93,"y":143,"fill":80},"210","myplugin.utils",[58,146],{"x":147,"y":148,"width":149,"height":150,"rx":96,"fill":97,"stroke":125,"style":99},"376","60","368","80",[82,152,156],{"x":153,"y":154,"style":155,"fill":125,"textAnchor":88},"560","86","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","re-import the package only",[82,158,160],{"x":153,"y":120,"style":159,"fill":80,"textAnchor":88},"text-anchor:middle;font-size:11px;font-family:sans-serif","returns the cached object",[82,162,164],{"x":153,"y":163,"style":159,"fill":125,"textAnchor":88},"126","edits to tools.py are invisible",[58,166],{"x":147,"y":167,"width":149,"height":150,"rx":96,"fill":97,"stroke":168,"style":99},"156","#15803d",[82,170,172],{"x":153,"y":171,"style":155,"fill":168,"textAnchor":88},"182","delete every myplugin.* entry",[82,174,176],{"x":153,"y":175,"style":159,"fill":80,"textAnchor":88},"204","next import re-reads from disk",[82,178,180],{"x":153,"y":179,"style":159,"fill":168,"textAnchor":88},"222","every change picked up",[182,183],"line",{"x1":184,"y1":185,"x2":186,"y2":187,"stroke":125,"style":188},"316","104","372","98","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#reloadArrow)",[182,190],{"x1":184,"y1":191,"x2":186,"y2":140,"stroke":168,"style":192},"170","stroke-width:2.5;marker-end:url(#reloadArrow)",[194,195,197],"h2",{"id":196},"prerequisites","Prerequisites",[199,200,201,209,216],"ul",{},[202,203,204,208],"li",{},[205,206,207],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[202,210,211,212,215],{},"A plugin under development in your profile's ",[18,213,214],{},"python\u002Fplugins"," directory.",[202,217,218,219,221,222,225],{},"An ",[18,220,38],{}," method that genuinely undoes what ",[18,223,224],{},"initGui()"," did — this is the part everything else depends on.",[194,227,229],{"id":228},"install-plugin-reloader","Install Plugin Reloader",[14,231,232],{},"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.",[234,235,236,243,250,256],"ol",{},[202,237,238,239,242],{},"Open ",[18,240,241],{},"Plugins > Manage and Install Plugins",".",[202,244,245,246,249],{},"Search for ",[205,247,248],{},"Plugin Reloader"," and install it.",[202,251,238,252,255],{},[18,253,254],{},"Plugins > Plugin Reloader > Choose a plugin to be reloaded"," and pick yours.",[202,257,258,259,262],{},"Press ",[205,260,261],{},"F5",", or click its toolbar button, after every edit.",[14,264,265,266,268,269,271,272,274,275,277],{},"Plugin Reloader calls your plugin's ",[18,267,38],{},", purges its modules from ",[18,270,20],{},", re-imports the package, and calls ",[18,273,224],{}," again. Everything that makes a reload go wrong is therefore in your ",[18,276,38],{}," — the reloader does its half correctly.",[194,279,281],{"id":280},"reload-from-the-console","Reload from the console",[14,283,284],{},"When you would rather not depend on another plugin, or need the reload inside a script, the same sequence is a handful of lines.",[286,287,292],"pre",{"className":288,"code":289,"language":290,"meta":291,"style":291},"language-python shiki shiki-themes github-dark","import sys\nfrom qgis import utils\n\nPLUGIN = \"myplugin\"\n\nutils.unloadPlugin(PLUGIN)\n\nfor name in list(sys.modules):\n    if name == PLUGIN or name.startswith(PLUGIN + \".\"):\n        del sys.modules[name]\n\nutils.loadPlugin(PLUGIN)\nutils.startPlugin(PLUGIN)\n","python","",[18,293,294,306,320,327,341,346,357,362,380,411,420,425,435],{"__ignoreMap":291},[295,296,298,302],"span",{"class":182,"line":297},1,[295,299,301],{"class":300},"snl16","import",[295,303,305],{"class":304},"s95oV"," sys\n",[295,307,309,312,315,317],{"class":182,"line":308},2,[295,310,311],{"class":300},"from",[295,313,314],{"class":304}," qgis ",[295,316,301],{"class":300},[295,318,319],{"class":304}," utils\n",[295,321,323],{"class":182,"line":322},3,[295,324,326],{"emptyLinePlaceholder":325},true,"\n",[295,328,330,334,337],{"class":182,"line":329},4,[295,331,333],{"class":332},"sDLfK","PLUGIN",[295,335,336],{"class":300}," =",[295,338,340],{"class":339},"sU2Wk"," \"myplugin\"\n",[295,342,344],{"class":182,"line":343},5,[295,345,326],{"emptyLinePlaceholder":325},[295,347,349,352,354],{"class":182,"line":348},6,[295,350,351],{"class":304},"utils.unloadPlugin(",[295,353,333],{"class":332},[295,355,356],{"class":304},")\n",[295,358,360],{"class":182,"line":359},7,[295,361,326],{"emptyLinePlaceholder":325},[295,363,365,368,371,374,377],{"class":182,"line":364},8,[295,366,367],{"class":300},"for",[295,369,370],{"class":304}," name ",[295,372,373],{"class":300},"in",[295,375,376],{"class":332}," list",[295,378,379],{"class":304},"(sys.modules):\n",[295,381,383,386,388,391,394,397,400,402,405,408],{"class":182,"line":382},9,[295,384,385],{"class":300},"    if",[295,387,370],{"class":304},[295,389,390],{"class":300},"==",[295,392,393],{"class":332}," PLUGIN",[295,395,396],{"class":300}," or",[295,398,399],{"class":304}," name.startswith(",[295,401,333],{"class":332},[295,403,404],{"class":300}," +",[295,406,407],{"class":339}," \".\"",[295,409,410],{"class":304},"):\n",[295,412,414,417],{"class":182,"line":413},10,[295,415,416],{"class":300},"        del",[295,418,419],{"class":304}," sys.modules[name]\n",[295,421,423],{"class":182,"line":422},11,[295,424,326],{"emptyLinePlaceholder":325},[295,426,428,431,433],{"class":182,"line":427},12,[295,429,430],{"class":304},"utils.loadPlugin(",[295,432,333],{"class":332},[295,434,356],{"class":304},[295,436,438,441,443],{"class":182,"line":437},13,[295,439,440],{"class":304},"utils.startPlugin(",[295,442,333],{"class":332},[295,444,356],{"class":304},[14,446,447,450,451,454,455,457,458,461,462,465,466,469,470,473,474,477,478,481,482,485,486,489,490,242],{},[205,448,449],{},"Breakdown:"," ",[18,452,453],{},"unloadPlugin()"," calls your ",[18,456,38],{}," and removes the plugin instance from the registry. The loop is the part a naive reload misses: iterating over ",[18,459,460],{},"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 ",[18,463,464],{},"myplugin."," prefix clears the package ",[23,467,468],{},"and"," every sub-module. Testing ",[18,471,472],{},"startswith(PLUGIN + \".\")"," rather than ",[18,475,476],{},"startswith(PLUGIN)"," avoids also deleting an unrelated ",[18,479,480],{},"myplugintools"," package. ",[18,483,484],{},"loadPlugin()"," re-imports and ",[18,487,488],{},"startPlugin()"," calls ",[18,491,224],{},[194,493,495],{"id":494},"write-an-unload-that-actually-undoes-things","Write an unload that actually undoes things",[14,497,498],{},[43,499,502,505,508,511,518,521,526,528,603,609,612,615,618,621],{"viewBox":500,"role":46,"ariaLabel":501,"xmlns":48},"0 0 760 264","A checklist pairing each thing initGui creates with the call in unload that removes it: actions, menus, map tools, signals, dock widgets and expression functions",[50,503,504],{},"Everything initGui() creates, unload() must remove",[54,506,507],{},"Six 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.",[58,509],{"x":60,"y":60,"width":61,"height":510,"fill":63},"264",[65,512,513],{},[68,514,516],{"id":515,"viewBox":71,"refX":72,"refY":73,"markerWidth":74,"markerHeight":74,"orient":75},"reloadPairArrow",[77,517],{"d":79,"fill":168},[82,519,520],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"Every line in initGui() needs its mirror in unload()",[82,522,224],{"x":523,"y":524,"style":155,"fill":525,"textAnchor":88},"200","62","#2563eb",[82,527,38],{"x":153,"y":524,"style":155,"fill":168,"textAnchor":88},[106,529,530,535,540,543,547,550,553,555,558,561,564,566,569,571,575,577,580,583,587,589,592,595,598,600],{"style":108},[58,531],{"x":111,"y":532,"width":533,"height":85,"rx":114,"fill":97,"stroke":525,"style":534},"74","320","stroke-width:1.6",[82,536,539],{"x":537,"y":538,"fill":80},"54","92","iface.addToolBarIcon(action)",[58,541],{"x":542,"y":532,"width":533,"height":85,"rx":114,"fill":97,"stroke":168,"style":534},"400",[82,544,546],{"x":545,"y":538,"fill":80},"414","iface.removeToolBarIcon(action)",[58,548],{"x":111,"y":549,"width":533,"height":85,"rx":114,"fill":97,"stroke":525,"style":534},"106",[82,551,552],{"x":537,"y":124,"fill":80},"iface.addPluginToMenu(...)",[58,554],{"x":542,"y":549,"width":533,"height":85,"rx":114,"fill":97,"stroke":168,"style":534},[82,556,557],{"x":545,"y":124,"fill":80},"iface.removePluginMenu(...)",[58,559],{"x":111,"y":560,"width":533,"height":85,"rx":114,"fill":97,"stroke":525,"style":534},"138",[82,562,563],{"x":537,"y":167,"fill":80},"canvas.setMapTool(self.tool)",[58,565],{"x":542,"y":560,"width":533,"height":85,"rx":114,"fill":97,"stroke":168,"style":534},[82,567,568],{"x":545,"y":167,"fill":80},"canvas.unsetMapTool(self.tool)",[58,570],{"x":111,"y":191,"width":533,"height":85,"rx":114,"fill":97,"stroke":525,"style":534},[82,572,574],{"x":537,"y":573,"fill":80},"188","signal.connect(handler)",[58,576],{"x":542,"y":191,"width":533,"height":85,"rx":114,"fill":97,"stroke":168,"style":534},[82,578,579],{"x":545,"y":573,"fill":80},"signal.disconnect(handler)",[58,581],{"x":111,"y":582,"width":533,"height":85,"rx":114,"fill":97,"stroke":525,"style":534},"202",[82,584,586],{"x":537,"y":585,"fill":80},"220","iface.addDockWidget(...)",[58,588],{"x":542,"y":582,"width":533,"height":85,"rx":114,"fill":97,"stroke":168,"style":534},[82,590,591],{"x":545,"y":585,"fill":80},"iface.removeDockWidget(...)",[58,593],{"x":111,"y":594,"width":533,"height":85,"rx":114,"fill":97,"stroke":525,"style":534},"234",[82,596,597],{"x":537,"y":113,"fill":80},"QgsExpression.registerFunction(f)",[58,599],{"x":542,"y":594,"width":533,"height":85,"rx":114,"fill":97,"stroke":168,"style":534},[82,601,602],{"x":545,"y":113,"fill":80},"QgsExpression.unregisterFunction(n)",[182,604],{"x1":605,"y1":606,"x2":607,"y2":606,"stroke":168,"style":608},"360","87","396","stroke-width:1.8;marker-end:url(#reloadPairArrow)",[182,610],{"x1":605,"y1":611,"x2":607,"y2":611,"stroke":168,"style":608},"119",[182,613],{"x1":605,"y1":614,"x2":607,"y2":614,"stroke":168,"style":608},"151",[182,616],{"x1":605,"y1":617,"x2":607,"y2":617,"stroke":168,"style":608},"183",[182,619],{"x1":605,"y1":620,"x2":607,"y2":620,"stroke":168,"style":608},"215",[182,622],{"x1":605,"y1":623,"x2":607,"y2":623,"stroke":168,"style":608},"247",[286,625,627],{"className":288,"code":626,"language":290,"meta":291,"style":291},"from qgis.core import QgsExpression, QgsProject\n\n\nclass MyPlugin:\n    def unload(self):\n        canvas = self.iface.mapCanvas()\n        if canvas.mapTool() is self.tool:\n            canvas.unsetMapTool(self.tool)\n\n        for signal, slot in self._connections:\n            try:\n                signal.disconnect(slot)\n            except TypeError:\n                pass\n        self._connections.clear()\n\n        try:\n            QgsExpression.unregisterFunction(\"myplugin_utm_zone\")\n        except Exception:\n            pass\n\n        if self.dock is not None:\n            self.iface.removeDockWidget(self.dock)\n            self.dock.deleteLater()\n            self.dock = None\n\n        for action in self.actions:\n            self.iface.removePluginMenu(\"&My Plugin\", action)\n            self.iface.removeToolBarIcon(action)\n            action.deleteLater()\n        self.actions.clear()\n\n        self.tool = None\n",[18,628,629,641,645,649,661,672,686,702,713,717,732,739,744,754,760,769,774,782,793,804,810,815,835,849,857,869,874,889,903,911,917,925,930],{"__ignoreMap":291},[295,630,631,633,636,638],{"class":182,"line":297},[295,632,311],{"class":300},[295,634,635],{"class":304}," qgis.core ",[295,637,301],{"class":300},[295,639,640],{"class":304}," QgsExpression, QgsProject\n",[295,642,643],{"class":182,"line":308},[295,644,326],{"emptyLinePlaceholder":325},[295,646,647],{"class":182,"line":322},[295,648,326],{"emptyLinePlaceholder":325},[295,650,651,654,658],{"class":182,"line":329},[295,652,653],{"class":300},"class",[295,655,657],{"class":656},"svObZ"," MyPlugin",[295,659,660],{"class":304},":\n",[295,662,663,666,669],{"class":182,"line":343},[295,664,665],{"class":300},"    def",[295,667,668],{"class":656}," unload",[295,670,671],{"class":304},"(self):\n",[295,673,674,677,680,683],{"class":182,"line":348},[295,675,676],{"class":304},"        canvas ",[295,678,679],{"class":300},"=",[295,681,682],{"class":332}," self",[295,684,685],{"class":304},".iface.mapCanvas()\n",[295,687,688,691,694,697,699],{"class":182,"line":359},[295,689,690],{"class":300},"        if",[295,692,693],{"class":304}," canvas.mapTool() ",[295,695,696],{"class":300},"is",[295,698,682],{"class":332},[295,700,701],{"class":304},".tool:\n",[295,703,704,707,710],{"class":182,"line":364},[295,705,706],{"class":304},"            canvas.unsetMapTool(",[295,708,709],{"class":332},"self",[295,711,712],{"class":304},".tool)\n",[295,714,715],{"class":182,"line":382},[295,716,326],{"emptyLinePlaceholder":325},[295,718,719,722,725,727,729],{"class":182,"line":413},[295,720,721],{"class":300},"        for",[295,723,724],{"class":304}," signal, slot ",[295,726,373],{"class":300},[295,728,682],{"class":332},[295,730,731],{"class":304},"._connections:\n",[295,733,734,737],{"class":182,"line":422},[295,735,736],{"class":300},"            try",[295,738,660],{"class":304},[295,740,741],{"class":182,"line":427},[295,742,743],{"class":304},"                signal.disconnect(slot)\n",[295,745,746,749,752],{"class":182,"line":437},[295,747,748],{"class":300},"            except",[295,750,751],{"class":332}," TypeError",[295,753,660],{"class":304},[295,755,757],{"class":182,"line":756},14,[295,758,759],{"class":300},"                pass\n",[295,761,763,766],{"class":182,"line":762},15,[295,764,765],{"class":332},"        self",[295,767,768],{"class":304},"._connections.clear()\n",[295,770,772],{"class":182,"line":771},16,[295,773,326],{"emptyLinePlaceholder":325},[295,775,777,780],{"class":182,"line":776},17,[295,778,779],{"class":300},"        try",[295,781,660],{"class":304},[295,783,785,788,791],{"class":182,"line":784},18,[295,786,787],{"class":304},"            QgsExpression.unregisterFunction(",[295,789,790],{"class":339},"\"myplugin_utm_zone\"",[295,792,356],{"class":304},[295,794,796,799,802],{"class":182,"line":795},19,[295,797,798],{"class":300},"        except",[295,800,801],{"class":332}," Exception",[295,803,660],{"class":304},[295,805,807],{"class":182,"line":806},20,[295,808,809],{"class":300},"            pass\n",[295,811,813],{"class":182,"line":812},21,[295,814,326],{"emptyLinePlaceholder":325},[295,816,818,820,822,825,827,830,833],{"class":182,"line":817},22,[295,819,690],{"class":300},[295,821,682],{"class":332},[295,823,824],{"class":304},".dock ",[295,826,696],{"class":300},[295,828,829],{"class":300}," not",[295,831,832],{"class":332}," None",[295,834,660],{"class":304},[295,836,838,841,844,846],{"class":182,"line":837},23,[295,839,840],{"class":332},"            self",[295,842,843],{"class":304},".iface.removeDockWidget(",[295,845,709],{"class":332},[295,847,848],{"class":304},".dock)\n",[295,850,852,854],{"class":182,"line":851},24,[295,853,840],{"class":332},[295,855,856],{"class":304},".dock.deleteLater()\n",[295,858,860,862,864,866],{"class":182,"line":859},25,[295,861,840],{"class":332},[295,863,824],{"class":304},[295,865,679],{"class":300},[295,867,868],{"class":332}," None\n",[295,870,872],{"class":182,"line":871},26,[295,873,326],{"emptyLinePlaceholder":325},[295,875,877,879,882,884,886],{"class":182,"line":876},27,[295,878,721],{"class":300},[295,880,881],{"class":304}," action ",[295,883,373],{"class":300},[295,885,682],{"class":332},[295,887,888],{"class":304},".actions:\n",[295,890,892,894,897,900],{"class":182,"line":891},28,[295,893,840],{"class":332},[295,895,896],{"class":304},".iface.removePluginMenu(",[295,898,899],{"class":339},"\"&My Plugin\"",[295,901,902],{"class":304},", action)\n",[295,904,906,908],{"class":182,"line":905},29,[295,907,840],{"class":332},[295,909,910],{"class":304},".iface.removeToolBarIcon(action)\n",[295,912,914],{"class":182,"line":913},30,[295,915,916],{"class":304},"            action.deleteLater()\n",[295,918,920,922],{"class":182,"line":919},31,[295,921,765],{"class":332},[295,923,924],{"class":304},".actions.clear()\n",[295,926,928],{"class":182,"line":927},32,[295,929,326],{"emptyLinePlaceholder":325},[295,931,933,935,938,940],{"class":182,"line":932},33,[295,934,765],{"class":332},[295,936,937],{"class":304},".tool ",[295,939,679],{"class":300},[295,941,868],{"class":332},[14,943,944,946,947,950,951,954,955,958,959,962,963,965,966,969,970,973,974,978,979,983],{},[205,945,449],{}," Keeping ",[18,948,949],{},"self._connections"," as a list of ",[18,952,953],{},"(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. ",[18,956,957],{},"try","\u002F",[18,960,961],{},"except TypeError"," makes each disconnect safe when the connection has already gone, so ",[18,964,38],{}," never half-fails. ",[18,967,968],{},"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 ",[18,971,972],{},"None"," releases the last Python references, which is what allows the module purge to actually free the old code. See ",[31,975,977],{"href":976},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","Connect to Layer and Project Signals in PyQGIS"," for the signal side and ",[31,980,982],{"href":981},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","Create a Custom Map Tool in PyQGIS"," for the tool side.",[194,985,987],{"id":986},"verify-the-reload-worked","Verify the reload worked",[14,989,990],{},"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.",[286,992,994],{"className":288,"code":993,"language":290,"meta":291,"style":291},"# in __init__.py\n__build__ = \"2026-08-01T09:14\"\n\n\ndef classFactory(iface):\n    from qgis.core import QgsMessageLog, Qgis\n    QgsMessageLog.logMessage(f\"myplugin loaded, build {__build__}\", \"myplugin\", Qgis.Info)\n    from .plugin import MyPlugin\n    return MyPlugin(iface)\n",[18,995,996,1002,1012,1016,1020,1031,1043,1075,1087],{"__ignoreMap":291},[295,997,998],{"class":182,"line":297},[295,999,1001],{"class":1000},"sjoCn","# in __init__.py\n",[295,1003,1004,1007,1009],{"class":182,"line":308},[295,1005,1006],{"class":304},"__build__ ",[295,1008,679],{"class":300},[295,1010,1011],{"class":339}," \"2026-08-01T09:14\"\n",[295,1013,1014],{"class":182,"line":322},[295,1015,326],{"emptyLinePlaceholder":325},[295,1017,1018],{"class":182,"line":329},[295,1019,326],{"emptyLinePlaceholder":325},[295,1021,1022,1025,1028],{"class":182,"line":343},[295,1023,1024],{"class":300},"def",[295,1026,1027],{"class":656}," classFactory",[295,1029,1030],{"class":304},"(iface):\n",[295,1032,1033,1036,1038,1040],{"class":182,"line":348},[295,1034,1035],{"class":300},"    from",[295,1037,635],{"class":304},[295,1039,301],{"class":300},[295,1041,1042],{"class":304}," QgsMessageLog, Qgis\n",[295,1044,1045,1048,1051,1054,1057,1060,1063,1066,1069,1072],{"class":182,"line":359},[295,1046,1047],{"class":304},"    QgsMessageLog.logMessage(",[295,1049,1050],{"class":300},"f",[295,1052,1053],{"class":339},"\"myplugin loaded, build ",[295,1055,1056],{"class":332},"{",[295,1058,1059],{"class":304},"__build__",[295,1061,1062],{"class":332},"}",[295,1064,1065],{"class":339},"\"",[295,1067,1068],{"class":304},", ",[295,1070,1071],{"class":339},"\"myplugin\"",[295,1073,1074],{"class":304},", Qgis.Info)\n",[295,1076,1077,1079,1082,1084],{"class":182,"line":364},[295,1078,1035],{"class":300},[295,1080,1081],{"class":304}," .plugin ",[295,1083,301],{"class":300},[295,1085,1086],{"class":304}," MyPlugin\n",[295,1088,1089,1092],{"class":182,"line":382},[295,1090,1091],{"class":300},"    return",[295,1093,1094],{"class":304}," MyPlugin(iface)\n",[14,1096,1097,1099,1100,1103,1104,1106,1107,450,1110,450,1113,1115],{},[205,1098,449],{}," Logging at ",[18,1101,1102],{},"classFactory"," time means the message appears on every load and reload, so the Log Messages panel becomes a reload history. Bumping ",[18,1105,1059],{}," — or deriving it from the file's modification time — makes a stale reload obvious immediately. Importing ",[18,1108,1109],{},"MyPlugin",[23,1111,1112],{},"inside",[18,1114,1102],{}," 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.",[194,1117,1119],{"id":1118},"what-a-reload-cannot-recover","What a reload cannot recover",[14,1121,1122],{},"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.",[14,1124,1125],{},[43,1126,1129,1132,1135,1138,1141,1146,1151,1172,1175,1179],{"viewBox":1127,"role":46,"ariaLabel":1128,"xmlns":48},"0 0 760 244","Two columns showing what a reload refreshes — Python modules, classes and instances — and what it does not: compiled resources, cached settings, loaded dialogs and registered providers",[50,1130,1131],{},"What a reload refreshes and what it does not",[54,1133,1134],{},"A 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.",[58,1136],{"x":60,"y":60,"width":61,"height":1137,"fill":63},"244",[82,1139,1140],{"x":84,"y":85,"style":86,"fill":87,"textAnchor":88},"A reload is a Python reload — nothing more",[58,1142],{"x":92,"y":1143,"width":1144,"height":1145,"rx":96,"fill":97,"stroke":168,"style":99},"46","356","180",[82,1147,1150],{"x":1148,"y":1149,"style":104,"fill":168,"textAnchor":88},"194","70","refreshed",[106,1152,1154,1159,1163,1166,1169],{"style":1153},"font-size:11.5px;font-family:sans-serif",[82,1155,1158],{"x":1156,"y":1157,"fill":80},"44","100","✓  every .py file in the package",[82,1160,1162],{"x":1156,"y":1161,"fill":80},"128","✓  classes and their methods",[82,1164,1165],{"x":1156,"y":167,"fill":80},"✓  the plugin instance itself",[82,1167,1168],{"x":1156,"y":95,"fill":80},"✓  module-level constants",[82,1170,1171],{"x":1156,"y":143,"fill":98},"this is the part that works",[58,1173],{"x":1174,"y":1143,"width":1144,"height":1145,"rx":96,"fill":97,"stroke":115,"style":99},"388",[82,1176,1178],{"x":1177,"y":1149,"style":104,"fill":115,"textAnchor":88},"566","not refreshed",[106,1180,1181,1185,1188,1191,1194],{"style":1153},[82,1182,1184],{"x":1183,"y":1157,"fill":80},"416","✗  compiled resources_rc.py",[82,1186,1187],{"x":1183,"y":1161,"fill":80},"✗  values already in QSettings",[82,1189,1190],{"x":1183,"y":167,"fill":80},"✗  a changed .ui file's C++ side",[82,1192,1193],{"x":1183,"y":95,"fill":80},"✗  a registered Processing provider",[82,1195,1196],{"x":1183,"y":143,"fill":115},"each needs its own action",[14,1198,1199,1200,1203,1204,1207,1208,1211,1212,1215],{},"The two that catch people most often have simple remedies. A changed icon or ",[18,1201,1202],{},".ui"," file needs its resource module rebuilding with ",[18,1205,1206],{},"pyrcc5"," before the reload will see it — and once ",[18,1209,1210],{},"resources_rc.py"," has been imported, it is cached like any other module, so it must be purged too. A setting written to ",[18,1213,1214],{},"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.",[14,1217,1218,1219,1221,1222,1225,1226,1230],{},"A Processing provider is the third case: it is registered with QGIS's provider registry rather than held by your plugin, so ",[18,1220,38],{}," must call ",[18,1223,1224],{},"QgsApplication.processingRegistry().removeProvider()"," or the reload will fail on a duplicate identifier. See ",[31,1227,1229],{"href":1228},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002F","Processing Provider Plugins"," for the registration lifecycle.",[194,1232,1234],{"id":1233},"qgis-version-compatibility","QGIS version compatibility",[14,1236,1237,1238,1240],{},"The examples target ",[205,1239,207],{}," (Python 3.12).",[1242,1243,1244,1260],"table",{},[1245,1246,1247],"thead",{},[1248,1249,1250,1254,1257],"tr",{},[1251,1252,1253],"th",{},"QGIS version",[1251,1255,1256],{},"Python",[1251,1258,1259],{},"Notes",[1261,1262,1263,1286,1297],"tbody",{},[1248,1264,1265,1269,1272],{},[1266,1267,1268],"td",{},"3.28 LTR",[1266,1270,1271],{},"3.9",[1266,1273,1274,1275,1278,1279,1278,1282,1285],{},"Identical. ",[18,1276,1277],{},"qgis.utils.unloadPlugin"," \u002F ",[18,1280,1281],{},"loadPlugin",[18,1283,1284],{},"startPlugin"," unchanged.",[1248,1287,1288,1291,1294],{},[1266,1289,1290],{},"3.34 LTR",[1266,1292,1293],{},"3.12",[1266,1295,1296],{},"Baseline for this page.",[1248,1298,1299,1302,1304],{},[1266,1300,1301],{},"3.40 \u002F 3.44",[1266,1303,1293],{},[1266,1305,1306,1309],{},[18,1307,1308],{},"qgis.utils.reloadPlugin()"," exists and performs the module purge for you.",[14,1311,1312,1313,1316],{},"Plugin Reloader is maintained separately from QGIS and works across all current 3.x releases. On 3.40 and newer, ",[18,1314,1315],{},"qgis.utils.reloadPlugin(\"myplugin\")"," is a one-line alternative to the console recipe.",[194,1318,1320],{"id":1319},"troubleshooting","Troubleshooting",[199,1322,1323,1332,1340,1349,1363,1373],{},[202,1324,1325,1328,1329,1331],{},[205,1326,1327],{},"The reload runs but the change is not there."," Only the package was re-imported. Delete every ",[18,1330,20],{}," entry starting with the plugin name plus a dot.",[202,1333,1334,1337,1338,242],{},[205,1335,1336],{},"Everything runs twice after a reload."," Signals from the previous instance are still connected. Record connections and disconnect them all in ",[18,1339,38],{},[202,1341,1342,450,1345,1348],{},[205,1343,1344],{},"Duplicate toolbar buttons appear.",[18,1346,1347],{},"removeToolBarIcon()"," was not called for every action created.",[202,1350,1351,1356,1357,1360,1361,242],{},[205,1352,1353,242],{},[18,1354,1355],{},"Function is already registered"," A custom expression function was not unregistered. Call ",[18,1358,1359],{},"QgsExpression.unregisterFunction()"," in ",[18,1362,38],{},[202,1364,1365,1368,1369,1372],{},[205,1366,1367],{},"QGIS crashes on reload."," A map tool was still installed when its Python object was released. Call ",[18,1370,1371],{},"unsetMapTool()"," before dropping the reference.",[202,1374,1375,450,1378,1381],{},[205,1376,1377],{},"The dock widget reappears twice.",[18,1379,1380],{},"removeDockWidget()"," was skipped, so the old widget is still parented to the main window.",[194,1383,1385],{"id":1384},"conclusion","Conclusion",[14,1387,1388,1389,1391,1392,1394],{},"A fast reload loop is worth setting up on day one, and it rests entirely on ",[18,1390,38],{}," being a true mirror of ",[18,1393,224],{},". 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.",[194,1396,1398],{"id":1397},"frequently-asked-questions","Frequently Asked Questions",[14,1400,1401,1404,1405,1407,1408,1411],{},[205,1402,1403],{},"Why does my plugin still run old code after reloading?","\nPython caches every module separately in ",[18,1406,20],{},". Re-importing the package returns the cached object without re-reading sub-modules, so a change in ",[18,1409,1410],{},"tools.py"," is invisible. Delete every entry whose name is the plugin package or starts with the package name plus a dot.",[14,1413,1414,1417,1418,1420,1421,1423],{},[205,1415,1416],{},"Do I need the Plugin Reloader plugin?","\nNo, but it is the least friction. It calls ",[18,1419,38],{},", purges the modules and re-runs ",[18,1422,224],{}," behind a keyboard shortcut. The same sequence is a handful of lines in the Python Console if you prefer no extra dependency.",[14,1425,1426,1429,1430,1432,1433,242],{},[205,1427,1428],{},"Why does everything happen twice after I reload?","\nThe previous instance's signal connections were never removed, so both the old and new handlers fire. Keep a list of ",[18,1431,953],{}," pairs as you connect them and disconnect them all in ",[18,1434,38],{},[14,1436,1437,1440,1441,1444],{},[205,1438,1439],{},"How can I be sure the reload actually picked up my change?","\nLog a build marker from ",[18,1442,1443],{},"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.",[14,1446,1447,1450],{},[205,1448,1449],{},"Does reloading a plugin reload its dependencies?","\nOnly 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.",[14,1452,1453,1456,1457,1459],{},[205,1454,1455],{},"Can I automate the reload on file save?","\nYes — Plugin Reloader offers a watch mode, and editors can be configured to trigger it. It is a considerable convenience once the ",[18,1458,38],{}," path is genuinely correct.",[194,1461,1463],{"id":1462},"related","Related",[199,1465,1466,1471,1475,1479],{},[202,1467,1468,1470],{},[31,1469,34],{"href":33}," — the guide this recipe belongs to",[202,1472,1473],{},[31,1474,982],{"href":981},[202,1476,1477],{},[31,1478,977],{"href":976},[202,1480,1481],{},[31,1482,1484],{"href":1483},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis\u002F","Register a Custom Expression Function in PyQGIS",[1486,1487,1488],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":291,"searchDepth":308,"depth":308,"links":1490},[1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501],{"id":196,"depth":308,"text":197},{"id":228,"depth":308,"text":229},{"id":280,"depth":308,"text":281},{"id":494,"depth":308,"text":495},{"id":986,"depth":308,"text":987},{"id":1118,"depth":308,"text":1119},{"id":1233,"depth":308,"text":1234},{"id":1319,"depth":308,"text":1320},{"id":1384,"depth":308,"text":1385},{"id":1397,"depth":308,"text":1398},{"id":1462,"depth":308,"text":1463},"Reload a QGIS plugin during development with Plugin Reloader or the console. Understand why Python caches modules, write a clean unload, and shorten the feedback loop.","md",{"slug":1505,"type":1506,"breadcrumb":1507,"datePublished":1508,"dateModified":1508},"reload-qgis-plugin-without-restart","article","Reload a Plugin","2026-08-01","\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart",{"title":5,"description":1502},"qgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002Findex","Elt9ltXXesiCBXwcB0dcdakNkAN3teHbdsOcSdmH90w",1785585990474]