[{"data":1,"prerenderedAt":1017},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis":3},{"id":4,"title":5,"body":6,"description":1006,"extension":1007,"meta":1008,"navigation":263,"path":1013,"seo":1014,"stem":1015,"__hash__":1016},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis\u002Findex.md","QGIS Object Ownership and Crashes in PyQGIS",{"type":7,"value":8,"toc":992},"minimark",[9,13,17,26,189,194,208,212,215,227,231,322,340,456,477,481,537,553,629,633,690,704,749,765,769,772,809,817,821,824,832,841,847,850,854,861,865,905,909,912,916,922,935,948,954,958,988],[10,11,5],"h1",{"id":12},"qgis-object-ownership-and-crashes-in-pyqgis",[14,15,16],"p",{},"A Python script should not be able to crash the application it runs in, and in most Python it cannot. PyQGIS is different: it is a binding over a large C++ library, and when a Python object is deleted while C++ still holds a pointer to it, QGIS does not raise — it segfaults, with no traceback and no message. Understanding the two ownership rules that cause almost all of these is a couple of hours that will save you days.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002F","QGIS API Architecture",". It covers how SIP maps C++ objects into Python, which calls transfer ownership, the three crash patterns you will actually hit, and how to make each impossible.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,101,107,111,115,122,127,131,135,137,140,144,148,152,155,158,162,166,168,170,173,175,178,185],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 336","img","Two ownership situations: Python owning an object that C++ merely borrows, and C++ taking ownership so Python must not delete it","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Who deletes the object decides whether you crash",[40,41,42],"desc",{},"When Python owns an object and C++ only borrows a pointer to it, letting the Python reference go out of scope deletes the object while C++ is still using it, and the next access crashes. When ownership is transferred to C++, the Python wrapper no longer controls the lifetime and the object survives, but touching it after C++ has deleted it crashes in the same way.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","336","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"ownArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","A segfault is a lifetime bug, every time",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"24","46","344","212","10","#fffdf7","#b91c1c","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"196","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","Python owns it — C++ borrows",[44,93],{"x":94,"y":95,"width":96,"height":79,"rx":97,"fill":98,"stroke":99,"style":100},"50","94","130","6","#f3e9d8","#b45309","stroke-width:2",[68,102,106],{"x":103,"y":104,"style":105,"fill":66,"textAnchor":74},"115","122","text-anchor:middle;font-size:10px;font-family:sans-serif","local variable",[44,108],{"x":81,"y":95,"width":96,"height":79,"rx":97,"fill":109,"stroke":110,"style":100},"#efeadd","#59645f",[68,112,114],{"x":113,"y":104,"style":105,"fill":66,"textAnchor":74},"277","C++ pointer",[116,117],"line",{"x1":118,"y1":119,"x2":120,"y2":119,"stroke":66,"style":121},"180","117","206","stroke-width:1.8;marker-end:url(#ownArrow)",[68,123,126],{"x":88,"y":124,"style":125,"fill":66,"textAnchor":74},"176","text-anchor:middle;font-size:10.5px;font-family:sans-serif","the function returns…",[44,128],{"x":94,"y":129,"width":96,"height":79,"rx":97,"fill":83,"stroke":84,"style":130},"192","stroke-width:2;stroke-dasharray:5 4",[68,132,134],{"x":103,"y":133,"style":105,"fill":84,"textAnchor":74},"220","collected",[44,136],{"x":81,"y":129,"width":96,"height":79,"rx":97,"fill":109,"stroke":110,"style":100},[68,138,139],{"x":113,"y":133,"style":105,"fill":84,"textAnchor":74},"dangling",[44,141],{"x":142,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":143,"style":85},"392","#15803d",[68,145,147],{"x":146,"y":89,"style":90,"fill":143,"textAnchor":74},"564","ownership transferred to C++",[44,149],{"x":150,"y":95,"width":96,"height":79,"rx":97,"fill":151,"stroke":143,"style":100},"418","#e8efe6",[68,153,106],{"x":154,"y":104,"style":105,"fill":66,"textAnchor":74},"483",[44,156],{"x":157,"y":95,"width":96,"height":79,"rx":97,"fill":151,"stroke":143,"style":100},"580",[68,159,161],{"x":160,"y":104,"style":105,"fill":66,"textAnchor":74},"645","C++ owns it",[116,163],{"x1":164,"y1":119,"x2":165,"y2":119,"stroke":66,"style":121},"548","574",[68,167,126],{"x":146,"y":124,"style":125,"fill":66,"textAnchor":74},[44,169],{"x":150,"y":129,"width":96,"height":79,"rx":97,"fill":83,"stroke":110,"style":130},[68,171,172],{"x":154,"y":133,"style":105,"fill":110,"textAnchor":74},"wrapper gone",[44,174],{"x":157,"y":129,"width":96,"height":79,"rx":97,"fill":151,"stroke":143,"style":100},[68,176,177],{"x":160,"y":133,"style":105,"fill":143,"textAnchor":74},"object still alive",[44,179],{"x":180,"y":181,"width":182,"height":183,"rx":58,"fill":184,"stroke":99,"style":100},"80","280","600","42","#fdf2e2",[68,186,188],{"x":70,"y":187,"style":125,"fill":66,"textAnchor":74},"306","the fix on the left is a reference that lives as long as C++ needs the object",[190,191,193],"h2",{"id":192},"prerequisites","Prerequisites",[195,196,197,205],"ul",{},[198,199,200,204],"li",{},[201,202,203],"strong",{},"QGIS 3.34 LTR"," or newer. Nothing here is version-specific; it is how the bindings work.",[198,206,207],{},"Familiarity with Python's reference counting — an object is deleted when the last reference to it goes.",[190,209,211],{"id":210},"how-the-bindings-work","How the bindings work",[14,213,214],{},"QGIS's C++ API is exposed to Python by SIP, which creates a thin Python wrapper around each C++ object. The wrapper holds a pointer and a flag saying who is responsible for deleting the underlying object. Most of the time that arrangement is invisible. It becomes visible in exactly two places: when you create an object in Python and hand it to QGIS, and when QGIS hands you an object it still owns.",[14,216,217,218,222,223,226],{},"The rule for the first case is that some methods take ownership and some do not, and the documentation marks the ones that do. ",[219,220,221],"code",{},"QgsProject.addMapLayer()"," takes ownership of the layer. ",[219,224,225],{},"QgsMapCanvas.setMapTool()"," does not. That single difference is why adding a layer to the project and forgetting about it is fine, while creating a map tool and forgetting about it crashes the moment the user clicks the canvas.",[190,228,230],{"id":229},"crash-pattern-one-the-collected-map-tool","Crash pattern one: the collected map tool",[232,233,238],"pre",{"className":234,"code":235,"language":236,"meta":237,"style":237},"language-python shiki shiki-themes github-dark","from qgis.gui import QgsMapToolEmitPoint\n\n# WRONG — the tool is deleted as soon as this scope ends\ndef enable_picking(canvas):\n    tool = QgsMapToolEmitPoint(canvas)\n    tool.canvasClicked.connect(lambda point, button: print(point))\n    canvas.setMapTool(tool)\n","python","",[219,239,240,258,265,272,285,297,316],{"__ignoreMap":237},[241,242,244,248,252,255],"span",{"class":116,"line":243},1,[241,245,247],{"class":246},"snl16","from",[241,249,251],{"class":250},"s95oV"," qgis.gui ",[241,253,254],{"class":246},"import",[241,256,257],{"class":250}," QgsMapToolEmitPoint\n",[241,259,261],{"class":116,"line":260},2,[241,262,264],{"emptyLinePlaceholder":263},true,"\n",[241,266,268],{"class":116,"line":267},3,[241,269,271],{"class":270},"sjoCn","# WRONG — the tool is deleted as soon as this scope ends\n",[241,273,275,278,282],{"class":116,"line":274},4,[241,276,277],{"class":246},"def",[241,279,281],{"class":280},"svObZ"," enable_picking",[241,283,284],{"class":250},"(canvas):\n",[241,286,288,291,294],{"class":116,"line":287},5,[241,289,290],{"class":250},"    tool ",[241,292,293],{"class":246},"=",[241,295,296],{"class":250}," QgsMapToolEmitPoint(canvas)\n",[241,298,300,303,306,309,313],{"class":116,"line":299},6,[241,301,302],{"class":250},"    tool.canvasClicked.connect(",[241,304,305],{"class":246},"lambda",[241,307,308],{"class":250}," point, button: ",[241,310,312],{"class":311},"sDLfK","print",[241,314,315],{"class":250},"(point))\n",[241,317,319],{"class":116,"line":318},7,[241,320,321],{"class":250},"    canvas.setMapTool(tool)\n",[14,323,324,327,328,331,332,335,336,339],{},[201,325,326],{},"Breakdown:"," ",[219,329,330],{},"setMapTool"," stores a bare pointer; it does not take ownership and does not add a Python reference. When ",[219,333,334],{},"enable_picking"," returns, ",[219,337,338],{},"tool"," is the only reference and it disappears, the wrapper is collected, the C++ object is deleted, and the canvas is left pointing at freed memory. The crash comes on the next click, which makes it look like a click-handling bug rather than a lifetime bug. The fix is to keep the reference alive as long as the tool is in use:",[232,341,343],{"className":234,"code":342,"language":236,"meta":237,"style":237},"class Picker:\n    def __init__(self, canvas):\n        self.canvas = canvas\n        self.tool = QgsMapToolEmitPoint(canvas)\n        self.tool.canvasClicked.connect(self.on_click)\n\n    def enable(self):\n        self.canvas.setMapTool(self.tool)\n\n    def on_click(self, point, button):\n        print(point.x(), point.y())\n",[219,344,345,356,367,380,391,404,408,418,431,436,447],{"__ignoreMap":237},[241,346,347,350,353],{"class":116,"line":243},[241,348,349],{"class":246},"class",[241,351,352],{"class":280}," Picker",[241,354,355],{"class":250},":\n",[241,357,358,361,364],{"class":116,"line":260},[241,359,360],{"class":246},"    def",[241,362,363],{"class":311}," __init__",[241,365,366],{"class":250},"(self, canvas):\n",[241,368,369,372,375,377],{"class":116,"line":267},[241,370,371],{"class":311},"        self",[241,373,374],{"class":250},".canvas ",[241,376,293],{"class":246},[241,378,379],{"class":250}," canvas\n",[241,381,382,384,387,389],{"class":116,"line":274},[241,383,371],{"class":311},[241,385,386],{"class":250},".tool ",[241,388,293],{"class":246},[241,390,296],{"class":250},[241,392,393,395,398,401],{"class":116,"line":287},[241,394,371],{"class":311},[241,396,397],{"class":250},".tool.canvasClicked.connect(",[241,399,400],{"class":311},"self",[241,402,403],{"class":250},".on_click)\n",[241,405,406],{"class":116,"line":299},[241,407,264],{"emptyLinePlaceholder":263},[241,409,410,412,415],{"class":116,"line":318},[241,411,360],{"class":246},[241,413,414],{"class":280}," enable",[241,416,417],{"class":250},"(self):\n",[241,419,421,423,426,428],{"class":116,"line":420},8,[241,422,371],{"class":311},[241,424,425],{"class":250},".canvas.setMapTool(",[241,427,400],{"class":311},[241,429,430],{"class":250},".tool)\n",[241,432,434],{"class":116,"line":433},9,[241,435,264],{"emptyLinePlaceholder":263},[241,437,439,441,444],{"class":116,"line":438},10,[241,440,360],{"class":246},[241,442,443],{"class":280}," on_click",[241,445,446],{"class":250},"(self, point, button):\n",[241,448,450,453],{"class":116,"line":449},11,[241,451,452],{"class":311},"        print",[241,454,455],{"class":250},"(point.x(), point.y())\n",[14,457,458,460,461,464,465,464,468,471,472,476],{},[201,459,326],{}," Holding the tool on an instance that itself lives — a plugin object, typically — keeps the reference count above zero for as long as the tool can be used. The same rule applies to ",[219,462,463],{},"QgsRubberBand",", ",[219,466,467],{},"QgsVertexMarker",[219,469,470],{},"QgsMapCanvasItem"," and every other canvas item: the canvas draws them and does not own them. Anything you create and hand to the canvas needs a home. The interaction between this and map tool design is covered in ",[21,473,475],{"href":474},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcreate-custom-map-tool-pyqgis\u002F","creating a custom map tool",".",[190,478,480],{"id":479},"crash-pattern-two-using-a-layer-after-the-project-deleted-it","Crash pattern two: using a layer after the project deleted it",[232,482,484],{"className":234,"code":483,"language":236,"meta":237,"style":237},"layer = QgsVectorLayer(\"\u002Fdata\u002Froads.gpkg\", \"roads\", \"ogr\")\nQgsProject.instance().addMapLayer(layer)\nQgsProject.instance().removeMapLayer(layer.id())\n\nprint(layer.featureCount())   # crash — the C++ object is gone\n",[219,485,486,513,518,523,527],{"__ignoreMap":237},[241,487,488,491,493,496,500,502,505,507,510],{"class":116,"line":243},[241,489,490],{"class":250},"layer ",[241,492,293],{"class":246},[241,494,495],{"class":250}," QgsVectorLayer(",[241,497,499],{"class":498},"sU2Wk","\"\u002Fdata\u002Froads.gpkg\"",[241,501,464],{"class":250},[241,503,504],{"class":498},"\"roads\"",[241,506,464],{"class":250},[241,508,509],{"class":498},"\"ogr\"",[241,511,512],{"class":250},")\n",[241,514,515],{"class":116,"line":260},[241,516,517],{"class":250},"QgsProject.instance().addMapLayer(layer)\n",[241,519,520],{"class":116,"line":267},[241,521,522],{"class":250},"QgsProject.instance().removeMapLayer(layer.id())\n",[241,524,525],{"class":116,"line":274},[241,526,264],{"emptyLinePlaceholder":263},[241,528,529,531,534],{"class":116,"line":287},[241,530,312],{"class":311},[241,532,533],{"class":250},"(layer.featureCount())   ",[241,535,536],{"class":270},"# crash — the C++ object is gone\n",[14,538,539,327,541,544,545,548,549,552],{},[201,540,326],{},[219,542,543],{},"addMapLayer"," transfers ownership to the project, so the project's ",[219,546,547],{},"removeMapLayer"," deletes the underlying object while the Python name ",[219,550,551],{},"layer"," still points at a wrapper for freed memory. Python cannot tell — the wrapper looks fine until you touch it. The habit that avoids it is to stop using a variable after removing the layer it names, and to look layers up from the project by id or name at the point of use rather than holding them across operations that might remove them.",[14,554,555],{},[29,556,559,562,565,568,571,574,577,582,585,589,593,598,600,603,607,610,613,616,619,626],{"viewBox":557,"role":32,"ariaLabel":558,"xmlns":34},"0 0 760 300","A checklist of API calls that transfer ownership to C++ and calls that do not, with the consequence of each",[36,560,561],{},"Which calls take ownership",[40,563,564],{},"Adding a layer to the project, adding a child to a layer tree group and adding a feature to a provider all transfer ownership, so the Python variable must not be used afterwards if the object may be removed. Setting a map tool, adding a canvas item and connecting a signal do not transfer ownership, so Python must keep a reference alive.",[44,566],{"x":46,"y":46,"width":47,"height":567,"fill":49},"300",[68,569,570],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Two lists worth memorising",[44,572],{"x":78,"y":79,"width":80,"height":88,"rx":82,"fill":573,"stroke":143,"style":85},"#edf8e9",[68,575,576],{"x":88,"y":89,"style":90,"fill":143,"textAnchor":74},"C++ takes ownership",[68,578,581],{"x":79,"y":579,"style":580,"fill":66},"104","font-size:10.5px;font-family:monospace","project.addMapLayer(layer)",[68,583,584],{"x":79,"y":96,"style":580,"fill":66},"group.addChildNode(node)",[68,586,588],{"x":79,"y":587,"style":580,"fill":66},"156","layout.addLayoutItem(item)",[68,590,592],{"x":79,"y":591,"style":580,"fill":66},"182","layer.setRenderer(renderer)",[68,594,597],{"x":79,"y":595,"style":596,"fill":110},"214","font-size:10px;font-family:sans-serif","do not reuse the variable after removal",[44,599],{"x":142,"y":79,"width":80,"height":88,"rx":82,"fill":83,"stroke":84,"style":85},[68,601,602],{"x":146,"y":89,"style":90,"fill":84,"textAnchor":74},"Python keeps ownership",[68,604,606],{"x":605,"y":579,"style":580,"fill":66},"414","canvas.setMapTool(tool)",[68,608,609],{"x":605,"y":96,"style":580,"fill":66},"QgsRubberBand(canvas)",[68,611,612],{"x":605,"y":587,"style":580,"fill":66},"QgsVertexMarker(canvas)",[68,614,615],{"x":605,"y":591,"style":580,"fill":66},"QTimer() in a plugin",[68,617,618],{"x":605,"y":595,"style":596,"fill":84},"store it on self, or it is collected",[44,620],{"x":621,"y":622,"width":623,"height":624,"rx":58,"fill":184,"stroke":99,"style":625},"120","258","520","34","stroke-width:1.8",[68,627,628],{"x":70,"y":181,"style":125,"fill":66,"textAnchor":74},"when in doubt, keep a reference — a leak is recoverable, a segfault is not",[190,630,632],{"id":631},"crash-pattern-three-the-dangling-signal-connection","Crash pattern three: the dangling signal connection",[232,634,636],{"className":234,"code":635,"language":236,"meta":237,"style":237},"class Watcher:\n    def __init__(self, layer):\n        layer.featureAdded.connect(self.on_added)\n\n    def on_added(self, feature_id):\n        self.update_panel(feature_id)     # self.panel may be gone\n",[219,637,638,647,656,666,670,680],{"__ignoreMap":237},[241,639,640,642,645],{"class":116,"line":243},[241,641,349],{"class":246},[241,643,644],{"class":280}," Watcher",[241,646,355],{"class":250},[241,648,649,651,653],{"class":116,"line":260},[241,650,360],{"class":246},[241,652,363],{"class":311},[241,654,655],{"class":250},"(self, layer):\n",[241,657,658,661,663],{"class":116,"line":267},[241,659,660],{"class":250},"        layer.featureAdded.connect(",[241,662,400],{"class":311},[241,664,665],{"class":250},".on_added)\n",[241,667,668],{"class":116,"line":274},[241,669,264],{"emptyLinePlaceholder":263},[241,671,672,674,677],{"class":116,"line":287},[241,673,360],{"class":246},[241,675,676],{"class":280}," on_added",[241,678,679],{"class":250},"(self, feature_id):\n",[241,681,682,684,687],{"class":116,"line":299},[241,683,371],{"class":311},[241,685,686],{"class":250},".update_panel(feature_id)     ",[241,688,689],{"class":270},"# self.panel may be gone\n",[14,691,692,694,695,699,700,703],{},[201,693,326],{}," A signal connection keeps the ",[696,697,698],"em",{},"slot's"," object alive from Qt's perspective only if the receiver is a QObject with a proper parent; a plain Python object connected this way can be collected while the connection survives, and the next emission calls into nothing. Worse, on plugin reload the old instance's connections are still live, so one signal fires two handlers and the second touches a destroyed widget. The discipline is to disconnect explicitly in ",[219,701,702],{},"unload",":",[232,705,707],{"className":234,"code":706,"language":236,"meta":237,"style":237},"    def teardown(self, layer):\n        try:\n            layer.featureAdded.disconnect(self.on_added)\n        except TypeError:\n            pass\n",[219,708,709,718,725,734,744],{"__ignoreMap":237},[241,710,711,713,716],{"class":116,"line":243},[241,712,360],{"class":246},[241,714,715],{"class":280}," teardown",[241,717,655],{"class":250},[241,719,720,723],{"class":116,"line":260},[241,721,722],{"class":246},"        try",[241,724,355],{"class":250},[241,726,727,730,732],{"class":116,"line":267},[241,728,729],{"class":250},"            layer.featureAdded.disconnect(",[241,731,400],{"class":311},[241,733,665],{"class":250},[241,735,736,739,742],{"class":116,"line":274},[241,737,738],{"class":246},"        except",[241,740,741],{"class":311}," TypeError",[241,743,355],{"class":250},[241,745,746],{"class":116,"line":287},[241,747,748],{"class":246},"            pass\n",[14,750,751,327,753,756,757,760,761,476],{},[201,752,326],{},[219,754,755],{},"disconnect"," raises ",[219,758,759],{},"TypeError"," if the connection is already gone, which happens routinely when the layer was removed first, so swallowing that specific exception is correct rather than lazy. Doing this for every connection a plugin makes is what allows the plugin to be reloaded during development without accumulating handlers — the mechanics of signals generally are covered in ",[21,762,764],{"href":763},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Funderstand-qgis-signals-and-slots-pyqgis\u002F","understanding QGIS signals and slots",[190,766,768],{"id":767},"diagnosing-a-crash-you-already-have","Diagnosing a crash you already have",[14,770,771],{},"There is no traceback, so the technique is bisection plus one diagnostic. Run QGIS from a terminal so any C++ output reaches you, and note the last thing the script did before dying — a crash on canvas interaction points at a collected canvas object, a crash on project close points at ownership, and a crash on plugin reload points at connections.",[232,773,775],{"className":234,"code":774,"language":236,"meta":237,"style":237},"import sip\n\nif sip.isdeleted(layer):\n    print(\"the underlying C++ object is gone\")\n",[219,776,777,784,788,796],{"__ignoreMap":237},[241,778,779,781],{"class":116,"line":243},[241,780,254],{"class":246},[241,782,783],{"class":250}," sip\n",[241,785,786],{"class":116,"line":260},[241,787,264],{"emptyLinePlaceholder":263},[241,789,790,793],{"class":116,"line":267},[241,791,792],{"class":246},"if",[241,794,795],{"class":250}," sip.isdeleted(layer):\n",[241,797,798,801,804,807],{"class":116,"line":274},[241,799,800],{"class":311},"    print",[241,802,803],{"class":250},"(",[241,805,806],{"class":498},"\"the underlying C++ object is gone\"",[241,808,512],{"class":250},[14,810,811,327,813,816],{},[201,812,326],{},[219,814,815],{},"sip.isdeleted()"," is the one direct check available and it is worth knowing: it reports whether the wrapper's C++ object has been destroyed, letting a defensive guard turn a crash into a handled case. It is a diagnostic rather than a design — code that needs it in production usually has a lifetime problem to fix rather than to detect — but during debugging it converts \"it crashes sometimes\" into a precise answer.",[190,818,820],{"id":819},"writing-code-that-cannot-hit-these","Writing code that cannot hit these",[14,822,823],{},"Three habits remove almost all the risk without any cleverness.",[14,825,826,829,830,476],{},[201,827,828],{},"Give every long-lived object an owner object."," A plugin class that holds its tools, its rubber bands, its timers and its dock widgets as attributes has, by construction, solved the reference problem for all of them. The rule is easy to apply because it needs no judgement about which calls transfer ownership: if you made it and QGIS is going to use it later, put it on ",[219,831,400],{},[14,833,834,327,837,840],{},[201,835,836],{},"Look layers up at the point of use.",[219,838,839],{},"QgsProject.instance().mapLayersByName(\"roads\")"," at the top of each function is a dictionary lookup and costs nothing, while a layer reference held on an instance across an unknown span of user activity is a bet that nobody removed it. Where the lookup returns an empty list, you get a clean Python error instead of a crash.",[14,842,843,846],{},[201,844,845],{},"Pair every connect with a disconnect and every add with a remove."," Writing the teardown at the same time as the setup, rather than afterwards, is what makes it complete — teardown written later is teardown written from memory.",[14,848,849],{},"None of these costs anything in performance or clarity, and together they turn a class of unexplained crashes into a class of ordinary Python errors that tell you what went wrong.",[190,851,853],{"id":852},"qgis-version-compatibility","QGIS version compatibility",[14,855,856,857,860],{},"None of this changes across QGIS 3 releases, because it is a property of the SIP bindings rather than of the API. What does change is which methods transfer ownership, occasionally, when a signature is revised; the C++ documentation marks transfer with an annotation, and the Python documentation carries it through. ",[219,858,859],{},"sip.isdeleted"," has been available throughout.",[190,862,864],{"id":863},"troubleshooting","Troubleshooting",[195,866,867,873,879,885,891,897],{},[198,868,869,872],{},[201,870,871],{},"QGIS closes instantly with no message."," A segfault. Run from a terminal to see any output.",[198,874,875,878],{},[201,876,877],{},"Crash on the first canvas click after enabling a tool."," The tool was not kept referenced.",[198,880,881,884],{},[201,882,883],{},"Crash when closing the project."," A variable still naming a layer the project deleted.",[198,886,887,890],{},[201,888,889],{},"Crash on plugin reload."," Signal connections from the previous instance still live.",[198,892,893,896],{},[201,894,895],{},"A rubber band disappears immediately."," Collected — store it on the plugin instance.",[198,898,899,904],{},[201,900,901,476],{},[219,902,903],{},"RuntimeError: wrapped C\u002FC++ object has been deleted"," The friendly version of the same bug; Qt widgets report it rather than crashing.",[190,906,908],{"id":907},"conclusion","Conclusion",[14,910,911],{},"Two rules cover almost everything: keep a Python reference to anything you create and hand to the canvas, and stop using a variable once the project owns and may delete what it names. Add a disciplined disconnect on unload and the third pattern disappears too. Crashes in PyQGIS feel mysterious precisely because there is no traceback, but the causes are few and each has a mechanical fix.",[190,913,915],{"id":914},"frequently-asked-questions","Frequently Asked Questions",[14,917,918,921],{},[201,919,920],{},"Why does the same code work in the console and crash in a plugin?","\nThe console keeps every name in a module-level namespace that persists, so objects that would be collected in a function stay alive. Moving working console code into a method is a classic way to introduce this bug.",[14,923,924,931,932,934],{},[201,925,926,927,930],{},"Does ",[219,928,929],{},"del"," on a layer variable delete the layer?","\nOnly if Python still owns it. Once the project owns it, ",[219,933,929],{}," drops the wrapper and leaves the layer in the project.",[14,936,937,940,943,944,947],{},[201,938,939],{},"Is there a way to force ownership?",[219,941,942],{},"sip.transferto()"," and ",[219,945,946],{},"sip.transferback()"," exist, and reaching for them is nearly always a sign that a reference should be kept somewhere sensible instead.",[14,949,950,953],{},[201,951,952],{},"Do these problems affect Processing scripts?","\nLess often, because algorithms create and consume objects within one call. The risk rises with anything that outlives a single function — tools, timers, panels and connections.",[190,955,957],{"id":956},"related","Related",[195,959,960,966,971,976,982],{},[198,961,962,965],{},[21,963,964],{"href":23},"QGIS API Architecture in PyQGIS"," — the guide this recipe belongs to",[198,967,968],{},[21,969,970],{"href":763},"Understand QGIS Signals and Slots in PyQGIS",[198,972,973],{},[21,974,975],{"href":474},"Create a Custom Map Tool in PyQGIS",[198,977,978],{},[21,979,981],{"href":980},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Fcatch-exceptions-and-tracebacks-pyqgis\u002F","Catch Exceptions and Tracebacks in PyQGIS",[198,983,984],{},[21,985,987],{"href":986},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","Reload a QGIS Plugin Without Restarting",[989,990,991],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":237,"searchDepth":260,"depth":260,"links":993},[994,995,996,997,998,999,1000,1001,1002,1003,1004,1005],{"id":192,"depth":260,"text":193},{"id":210,"depth":260,"text":211},{"id":229,"depth":260,"text":230},{"id":479,"depth":260,"text":480},{"id":631,"depth":260,"text":632},{"id":767,"depth":260,"text":768},{"id":819,"depth":260,"text":820},{"id":852,"depth":260,"text":853},{"id":863,"depth":260,"text":864},{"id":907,"depth":260,"text":908},{"id":914,"depth":260,"text":915},{"id":956,"depth":260,"text":957},"Why PyQGIS scripts segfault — C++ ownership transfer through SIP, objects garbage-collected while QGIS still uses them, dangling signal connections, and how to avoid all three.","md",{"slug":1009,"type":1010,"breadcrumb":1011,"datePublished":1012,"dateModified":1012},"qgis-object-ownership-and-crashes-pyqgis","article","Object Ownership & Crashes","2026-09-05","\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis",{"title":5,"description":1006},"pyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis\u002Findex","VgF5kO984pErpgk5YrEOkj_1WdrayZdyqKhwteB-yn8",1788563847742]