[{"data":1,"prerenderedAt":1585},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis":3},{"id":4,"title":5,"body":6,"description":1574,"extension":1575,"meta":1576,"navigation":247,"path":1581,"seo":1582,"stem":1583,"__hash__":1584},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis\u002Findex.md","Organise the Layer Tree with Groups in PyQGIS",{"type":7,"value":8,"toc":1561},"minimark",[9,13,17,26,179,184,211,215,410,432,435,461,476,480,483,563,574,648,652,655,864,888,892,895,965,973,1077,1081,1084,1314,1323,1327,1392,1406,1410,1469,1473,1487,1491,1497,1506,1512,1518,1524,1528,1557],[10,11,5],"h1",{"id":12},"organise-the-layer-tree-with-groups-in-pyqgis",[14,15,16],"p",{},"A project with forty layers and no groups is technically complete and practically unusable. Grouping is what turns a wall of names into a map somebody can navigate — base data, analysis outputs, annotation — and because the structure is part of the project file, a script that generates projects has to build it deliberately or hand the user the wall.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002F","Working with QGIS Projects in PyQGIS",". It covers the layer tree node API: creating groups, placing layers precisely, reordering, controlling visibility and expansion, and the mutually exclusive groups that turn a group into a radio-button switcher.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,98,103,109,115,120,124,127,131,134,138,142,147,151,153,156,162,165,170,172,175],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 276","img","Structure of the layer tree showing the root node containing a group of base layers, a group of analysis layers with a nested subgroup, and a single ungrouped layer, with the node types labelled","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"The layer tree is a tree of nodes, not a list of layers",[40,41,42],"desc",{},"The root node holds three children: a group named Base map containing two layer nodes, a group named Analysis containing one layer node and a nested subgroup, and one ungrouped layer node. Group nodes and layer nodes are different classes, and only layer nodes point at a registered map layer.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","276","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"treeNodeArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#59645f",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Groups nest; layers are always leaves",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"24","52","180","44","#eef7f4","#0f766e","stroke-width:2.5",[68,86,90],{"x":87,"y":88,"style":89,"fill":83,"textAnchor":74},"114","80","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","layerTreeRoot()",[44,92],{"x":93,"y":79,"width":94,"height":81,"rx":58,"fill":95,"stroke":96,"style":97},"248","200","#eff3ff","#2563eb","stroke-width:2",[68,99,102],{"x":100,"y":88,"style":101,"fill":96,"textAnchor":74},"348","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","group: Base map",[44,104],{"x":105,"y":79,"width":106,"height":81,"rx":58,"fill":107,"stroke":66,"style":108},"492","232","#fffdf7","stroke-width:1.5",[68,110,114],{"x":111,"y":88,"style":112,"fill":113,"textAnchor":74},"608","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","QgsLayerTreeGroup",[44,116],{"x":93,"y":117,"width":94,"height":118,"rx":119,"fill":107,"stroke":66,"style":108},"110","34","6",[68,121,123],{"x":100,"y":122,"style":112,"fill":113,"textAnchor":74},"132","Roads",[44,125],{"x":93,"y":126,"width":94,"height":118,"rx":119,"fill":107,"stroke":66,"style":108},"150",[68,128,130],{"x":100,"y":129,"style":112,"fill":113,"textAnchor":74},"172","Buildings",[44,132],{"x":105,"y":117,"width":106,"height":133,"rx":119,"fill":107,"stroke":66,"style":108},"74",[68,135,137],{"x":111,"y":136,"style":112,"fill":113,"textAnchor":74},"140","QgsLayerTreeLayer nodes",[68,139,141],{"x":111,"y":140,"style":112,"fill":66,"textAnchor":74},"162","each points at a registered layer",[44,143],{"x":93,"y":144,"width":94,"height":81,"rx":58,"fill":145,"stroke":146,"style":97},"198","#fdf2e2","#b45309",[68,148,150],{"x":100,"y":149,"style":101,"fill":146,"textAnchor":74},"226","group: Analysis",[44,152],{"x":105,"y":144,"width":106,"height":81,"rx":58,"fill":107,"stroke":146,"style":108},[68,154,155],{"x":111,"y":149,"style":112,"fill":113,"textAnchor":74},"may contain further groups",[157,158],"line",{"x1":159,"y1":133,"x2":160,"y2":133,"stroke":66,"style":161},"204","242","stroke-width:2;marker-end:url(#treeNodeArrow)",[157,163],{"x1":159,"y1":88,"x2":160,"y2":164,"stroke":66,"style":161},"214",[157,166],{"x1":167,"y1":133,"x2":168,"y2":133,"stroke":66,"style":169},"448","486","stroke-width:1.5;stroke-dasharray:4 3;marker-end:url(#treeNodeArrow)",[157,171],{"x1":167,"y1":136,"x2":168,"y2":136,"stroke":66,"style":169},[157,173],{"x1":167,"y1":174,"x2":168,"y2":174,"stroke":66,"style":169},"220",[68,176,178],{"x":70,"y":177,"style":112,"fill":66,"textAnchor":74},"266","Removing a node hides the layer; it does not remove the layer from the project",[180,181,183],"h2",{"id":182},"prerequisites","Prerequisites",[185,186,187,195,203],"ul",{},[188,189,190,194],"li",{},[191,192,193],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[188,196,197,198,202],{},"A project with a few layers already registered — see ",[21,199,201],{"href":200},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fadd-and-remove-layers-from-project-pyqgis\u002F","Add and Remove Layers from a Project in PyQGIS",".",[188,204,205,206,210],{},"The ",[207,208,209],"code",{},"qgis.core"," module; nothing here needs the GUI, so it works in headless scripts too.",[180,212,214],{"id":213},"create-groups-and-place-layers-in-them","Create groups and place layers in them",[216,217,222],"pre",{"className":218,"code":219,"language":220,"meta":221,"style":221},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nproject = QgsProject.instance()\nroot = project.layerTreeRoot()\n\nbase = root.insertGroup(0, \"Base map\")          # at the top\nanalysis = root.addGroup(\"Analysis\")            # at the bottom\n\nfor layer in project.mapLayersByName(\"Roads\") + project.mapLayersByName(\"Buildings\"):\n    node = root.findLayer(layer.id())\n    if node:\n        clone = node.clone()\n        base.insertChildNode(0, clone)\n        node.parent().removeChildNode(node)\n","python","",[207,223,224,242,249,261,272,277,305,325,330,362,373,382,393,404],{"__ignoreMap":221},[225,226,228,232,236,239],"span",{"class":157,"line":227},1,[225,229,231],{"class":230},"snl16","from",[225,233,235],{"class":234},"s95oV"," qgis.core ",[225,237,238],{"class":230},"import",[225,240,241],{"class":234}," QgsProject\n",[225,243,245],{"class":157,"line":244},2,[225,246,248],{"emptyLinePlaceholder":247},true,"\n",[225,250,252,255,258],{"class":157,"line":251},3,[225,253,254],{"class":234},"project ",[225,256,257],{"class":230},"=",[225,259,260],{"class":234}," QgsProject.instance()\n",[225,262,264,267,269],{"class":157,"line":263},4,[225,265,266],{"class":234},"root ",[225,268,257],{"class":230},[225,270,271],{"class":234}," project.layerTreeRoot()\n",[225,273,275],{"class":157,"line":274},5,[225,276,248],{"emptyLinePlaceholder":247},[225,278,280,283,285,288,291,294,298,301],{"class":157,"line":279},6,[225,281,282],{"class":234},"base ",[225,284,257],{"class":230},[225,286,287],{"class":234}," root.insertGroup(",[225,289,46],{"class":290},"sDLfK",[225,292,293],{"class":234},", ",[225,295,297],{"class":296},"sU2Wk","\"Base map\"",[225,299,300],{"class":234},")          ",[225,302,304],{"class":303},"sjoCn","# at the top\n",[225,306,308,311,313,316,319,322],{"class":157,"line":307},7,[225,309,310],{"class":234},"analysis ",[225,312,257],{"class":230},[225,314,315],{"class":234}," root.addGroup(",[225,317,318],{"class":296},"\"Analysis\"",[225,320,321],{"class":234},")            ",[225,323,324],{"class":303},"# at the bottom\n",[225,326,328],{"class":157,"line":327},8,[225,329,248],{"emptyLinePlaceholder":247},[225,331,333,336,339,342,345,348,351,354,356,359],{"class":157,"line":332},9,[225,334,335],{"class":230},"for",[225,337,338],{"class":234}," layer ",[225,340,341],{"class":230},"in",[225,343,344],{"class":234}," project.mapLayersByName(",[225,346,347],{"class":296},"\"Roads\"",[225,349,350],{"class":234},") ",[225,352,353],{"class":230},"+",[225,355,344],{"class":234},[225,357,358],{"class":296},"\"Buildings\"",[225,360,361],{"class":234},"):\n",[225,363,365,368,370],{"class":157,"line":364},10,[225,366,367],{"class":234},"    node ",[225,369,257],{"class":230},[225,371,372],{"class":234}," root.findLayer(layer.id())\n",[225,374,376,379],{"class":157,"line":375},11,[225,377,378],{"class":230},"    if",[225,380,381],{"class":234}," node:\n",[225,383,385,388,390],{"class":157,"line":384},12,[225,386,387],{"class":234},"        clone ",[225,389,257],{"class":230},[225,391,392],{"class":234}," node.clone()\n",[225,394,396,399,401],{"class":157,"line":395},13,[225,397,398],{"class":234},"        base.insertChildNode(",[225,400,46],{"class":290},[225,402,403],{"class":234},", clone)\n",[225,405,407],{"class":157,"line":406},14,[225,408,409],{"class":234},"        node.parent().removeChildNode(node)\n",[14,411,412,415,416,419,420,423,424,427,428,431],{},[191,413,414],{},"Breakdown:"," ",[207,417,418],{},"insertGroup(index, name)"," places a group at a known position while ",[207,421,422],{},"addGroup(name)"," appends, and both return the group node you then work with. Moving an existing layer node is the part that surprises people: nodes cannot be reparented directly, so the idiom is clone-then-remove — ",[207,425,426],{},"clone()"," copies the node including its visibility and any custom properties, ",[207,429,430],{},"insertChildNode()"," puts the copy where you want it, and removing the original leaves exactly one node. The underlying map layer is never touched by any of this, which is why the layer keeps its id, styling and data.",[14,433,434],{},"For a layer you are adding fresh, skip the dance entirely by registering it without a legend entry and inserting it straight into the group:",[216,436,438],{"className":218,"code":437,"language":220,"meta":221,"style":221},"project.addMapLayer(flood_extent, False)\nanalysis.insertLayer(0, flood_extent)\n",[207,439,440,451],{"__ignoreMap":221},[225,441,442,445,448],{"class":157,"line":227},[225,443,444],{"class":234},"project.addMapLayer(flood_extent, ",[225,446,447],{"class":290},"False",[225,449,450],{"class":234},")\n",[225,452,453,456,458],{"class":157,"line":244},[225,454,455],{"class":234},"analysis.insertLayer(",[225,457,46],{"class":290},[225,459,460],{"class":234},", flood_extent)\n",[14,462,463,415,465,468,469,471,472,475],{},[191,464,414],{},[207,466,467],{},"insertLayer()"," creates the tree node for you, so this is both shorter and cheaper than adding at the root and moving afterwards. Index ",[207,470,46],{}," is the top of the group; ",[207,473,474],{},"addLayer()"," appends to the bottom. Remember that in QGIS the top of the panel draws last, which means the first child of the tree is the layer drawn over everything else.",[180,477,479],{"id":478},"control-visibility-expansion-and-exclusivity","Control visibility, expansion and exclusivity",[14,481,482],{},"Every node carries display state that the project file remembers.",[216,484,486],{"className":218,"code":485,"language":220,"meta":221,"style":221},"base.setExpanded(False)                 # collapsed in the panel\nbase.setItemVisibilityChecked(True)     # the group's own checkbox\n\nanalysis.setItemVisibilityCheckedRecursive(False)   # uncheck the group and its children\n\nscenarios = root.addGroup(\"Scenarios\")\nscenarios.setIsMutuallyExclusive(True)  # only one child visible at a time\n",[207,487,488,501,515,519,532,536,550],{"__ignoreMap":221},[225,489,490,493,495,498],{"class":157,"line":227},[225,491,492],{"class":234},"base.setExpanded(",[225,494,447],{"class":290},[225,496,497],{"class":234},")                 ",[225,499,500],{"class":303},"# collapsed in the panel\n",[225,502,503,506,509,512],{"class":157,"line":244},[225,504,505],{"class":234},"base.setItemVisibilityChecked(",[225,507,508],{"class":290},"True",[225,510,511],{"class":234},")     ",[225,513,514],{"class":303},"# the group's own checkbox\n",[225,516,517],{"class":157,"line":251},[225,518,248],{"emptyLinePlaceholder":247},[225,520,521,524,526,529],{"class":157,"line":263},[225,522,523],{"class":234},"analysis.setItemVisibilityCheckedRecursive(",[225,525,447],{"class":290},[225,527,528],{"class":234},")   ",[225,530,531],{"class":303},"# uncheck the group and its children\n",[225,533,534],{"class":157,"line":274},[225,535,248],{"emptyLinePlaceholder":247},[225,537,538,541,543,545,548],{"class":157,"line":279},[225,539,540],{"class":234},"scenarios ",[225,542,257],{"class":230},[225,544,315],{"class":234},[225,546,547],{"class":296},"\"Scenarios\"",[225,549,450],{"class":234},[225,551,552,555,557,560],{"class":157,"line":307},[225,553,554],{"class":234},"scenarios.setIsMutuallyExclusive(",[225,556,508],{"class":290},[225,558,559],{"class":234},")  ",[225,561,562],{"class":303},"# only one child visible at a time\n",[14,564,565,415,567,570,571,573],{},[191,566,414],{},[207,568,569],{},"setExpanded()"," affects only how the panel looks when the project opens — worth setting to ",[207,572,447],{}," for a group of fifteen base layers nobody needs to see individually. The visibility calls are the checkboxes: the non-recursive form toggles just this node, while the recursive form pushes the state down to every descendant, which is what you want when switching a whole analysis group off. A mutually exclusive group behaves like a set of radio buttons: checking one child unchecks the others, which is the cleanest way to ship four modelled scenarios in one project without the user accidentally viewing two at once.",[14,575,576],{},[29,577,580,583,586,589,592,598,602,608,613,616,619,621,625,628,632,635,638,640,643,645],{"viewBox":578,"role":32,"ariaLabel":579,"xmlns":34},"0 0 760 240","Comparison of a normal group where several children can be checked at once against a mutually exclusive group where checking one scenario automatically unchecks the others",[36,581,582],{},"A mutually exclusive group behaves like radio buttons",[40,584,585],{},"On the left a normal group has three scenario layers, two of which are checked at the same time, producing an overlapping and misleading map. On the right the same group set to mutually exclusive shows only one scenario checked; checking a different one automatically clears the previous choice.",[44,587],{"x":46,"y":46,"width":47,"height":588,"fill":49},"240",[68,590,591],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Stop the user from viewing two scenarios at once",[44,593],{"x":78,"y":594,"width":595,"height":596,"rx":597,"fill":145,"stroke":146,"style":84},"48","330","168","10",[68,599,601],{"x":600,"y":133,"style":89,"fill":146,"textAnchor":74},"189","ordinary group",[44,603],{"x":604,"y":605,"width":606,"height":118,"rx":119,"fill":107,"stroke":607,"style":97},"46","88","286","#15803d",[68,609,612],{"x":610,"y":117,"style":611,"fill":113},"62","font-size:11px;font-family:sans-serif","checked — scenario A, 1 in 100",[44,614],{"x":604,"y":615,"width":606,"height":118,"rx":119,"fill":107,"stroke":607,"style":97},"128",[68,617,618],{"x":610,"y":126,"style":611,"fill":113},"checked — scenario B, 1 in 200",[44,620],{"x":604,"y":596,"width":606,"height":118,"rx":119,"fill":49,"stroke":66,"style":108},[68,622,624],{"x":610,"y":623,"style":611,"fill":66},"190","unchecked — scenario C",[44,626],{"x":627,"y":594,"width":595,"height":596,"rx":597,"fill":82,"stroke":83,"style":84},"406",[68,629,631],{"x":630,"y":133,"style":89,"fill":83,"textAnchor":74},"571","mutually exclusive group",[44,633],{"x":634,"y":605,"width":606,"height":118,"rx":119,"fill":107,"stroke":607,"style":97},"428",[68,636,612],{"x":637,"y":117,"style":611,"fill":113},"444",[44,639],{"x":634,"y":615,"width":606,"height":118,"rx":119,"fill":49,"stroke":66,"style":108},[68,641,642],{"x":637,"y":126,"style":611,"fill":66},"cleared automatically — scenario B",[44,644],{"x":634,"y":596,"width":606,"height":118,"rx":119,"fill":49,"stroke":66,"style":108},[68,646,647],{"x":637,"y":623,"style":611,"fill":66},"cleared automatically — scenario C",[180,649,651],{"id":650},"walk-the-tree","Walk the tree",[14,653,654],{},"Reading the structure is as useful as building it — for a report of what a project contains, or to apply something to every layer in one group.",[216,656,658],{"className":218,"code":657,"language":220,"meta":221,"style":221},"from qgis.core import QgsLayerTreeGroup, QgsLayerTreeLayer\n\ndef describe(node, depth=0):\n    pad = \"  \" * depth\n    for child in node.children():\n        if isinstance(child, QgsLayerTreeGroup):\n            print(f\"{pad}[{child.name()}]\")\n            describe(child, depth + 1)\n        elif isinstance(child, QgsLayerTreeLayer):\n            state = \"on\" if child.isVisible() else \"off\"\n            print(f\"{pad}- {child.name()} ({state})\")\n\ndescribe(project.layerTreeRoot())\n",[207,659,660,671,675,693,709,722,733,771,783,793,815,855,859],{"__ignoreMap":221},[225,661,662,664,666,668],{"class":157,"line":227},[225,663,231],{"class":230},[225,665,235],{"class":234},[225,667,238],{"class":230},[225,669,670],{"class":234}," QgsLayerTreeGroup, QgsLayerTreeLayer\n",[225,672,673],{"class":157,"line":244},[225,674,248],{"emptyLinePlaceholder":247},[225,676,677,680,684,687,689,691],{"class":157,"line":251},[225,678,679],{"class":230},"def",[225,681,683],{"class":682},"svObZ"," describe",[225,685,686],{"class":234},"(node, depth",[225,688,257],{"class":230},[225,690,46],{"class":290},[225,692,361],{"class":234},[225,694,695,698,700,703,706],{"class":157,"line":263},[225,696,697],{"class":234},"    pad ",[225,699,257],{"class":230},[225,701,702],{"class":296}," \"  \"",[225,704,705],{"class":230}," *",[225,707,708],{"class":234}," depth\n",[225,710,711,714,717,719],{"class":157,"line":274},[225,712,713],{"class":230},"    for",[225,715,716],{"class":234}," child ",[225,718,341],{"class":230},[225,720,721],{"class":234}," node.children():\n",[225,723,724,727,730],{"class":157,"line":279},[225,725,726],{"class":230},"        if",[225,728,729],{"class":290}," isinstance",[225,731,732],{"class":234},"(child, QgsLayerTreeGroup):\n",[225,734,735,738,741,744,747,750,753,756,759,761,764,766,769],{"class":157,"line":307},[225,736,737],{"class":290},"            print",[225,739,740],{"class":234},"(",[225,742,743],{"class":230},"f",[225,745,746],{"class":296},"\"",[225,748,749],{"class":290},"{",[225,751,752],{"class":234},"pad",[225,754,755],{"class":290},"}",[225,757,758],{"class":296},"[",[225,760,749],{"class":290},[225,762,763],{"class":234},"child.name()",[225,765,755],{"class":290},[225,767,768],{"class":296},"]\"",[225,770,450],{"class":234},[225,772,773,776,778,781],{"class":157,"line":327},[225,774,775],{"class":234},"            describe(child, depth ",[225,777,353],{"class":230},[225,779,780],{"class":290}," 1",[225,782,450],{"class":234},[225,784,785,788,790],{"class":157,"line":332},[225,786,787],{"class":230},"        elif",[225,789,729],{"class":290},[225,791,792],{"class":234},"(child, QgsLayerTreeLayer):\n",[225,794,795,798,800,803,806,809,812],{"class":157,"line":364},[225,796,797],{"class":234},"            state ",[225,799,257],{"class":230},[225,801,802],{"class":296}," \"on\"",[225,804,805],{"class":230}," if",[225,807,808],{"class":234}," child.isVisible() ",[225,810,811],{"class":230},"else",[225,813,814],{"class":296}," \"off\"\n",[225,816,817,819,821,823,825,827,829,831,834,836,838,840,843,845,848,850,853],{"class":157,"line":375},[225,818,737],{"class":290},[225,820,740],{"class":234},[225,822,743],{"class":230},[225,824,746],{"class":296},[225,826,749],{"class":290},[225,828,752],{"class":234},[225,830,755],{"class":290},[225,832,833],{"class":296},"- ",[225,835,749],{"class":290},[225,837,763],{"class":234},[225,839,755],{"class":290},[225,841,842],{"class":296}," (",[225,844,749],{"class":290},[225,846,847],{"class":234},"state",[225,849,755],{"class":290},[225,851,852],{"class":296},")\"",[225,854,450],{"class":234},[225,856,857],{"class":157,"line":384},[225,858,248],{"emptyLinePlaceholder":247},[225,860,861],{"class":157,"line":395},[225,862,863],{"class":234},"describe(project.layerTreeRoot())\n",[14,865,866,415,868,871,872,875,876,879,880,883,884,887],{},[191,867,414],{},[207,869,870],{},"children()"," returns the immediate children in panel order, so recursion is the natural traversal. Distinguishing the two node classes by type is the standard approach — a group node has no layer, and calling ",[207,873,874],{},"layer()"," on it returns ",[207,877,878],{},"None",". ",[207,881,882],{},"isVisible()"," on a layer node accounts for its parents: a checked layer inside an unchecked group reports as not visible, which is exactly the question you usually want answered. If you only need the layers and not the structure, ",[207,885,886],{},"root.findLayers()"," returns every layer node in the tree in one flat list.",[180,889,891],{"id":890},"order-layers-deliberately","Order layers deliberately",[14,893,894],{},"Draw order is tree order, and getting it wrong produces a map where the polygons hide the labels.",[216,896,898],{"className":218,"code":897,"language":220,"meta":221,"style":221},"def move_to_top(root, layer_id):\n    node = root.findLayer(layer_id)\n    if not node:\n        return\n    clone = node.clone()\n    root.insertChildNode(0, clone)\n    node.parent().removeChildNode(node)\n\nmove_to_top(project.layerTreeRoot(), annotation_layer.id())\n",[207,899,900,910,919,928,933,942,951,956,960],{"__ignoreMap":221},[225,901,902,904,907],{"class":157,"line":227},[225,903,679],{"class":230},[225,905,906],{"class":682}," move_to_top",[225,908,909],{"class":234},"(root, layer_id):\n",[225,911,912,914,916],{"class":157,"line":244},[225,913,367],{"class":234},[225,915,257],{"class":230},[225,917,918],{"class":234}," root.findLayer(layer_id)\n",[225,920,921,923,926],{"class":157,"line":251},[225,922,378],{"class":230},[225,924,925],{"class":230}," not",[225,927,381],{"class":234},[225,929,930],{"class":157,"line":263},[225,931,932],{"class":230},"        return\n",[225,934,935,938,940],{"class":157,"line":274},[225,936,937],{"class":234},"    clone ",[225,939,257],{"class":230},[225,941,392],{"class":234},[225,943,944,947,949],{"class":157,"line":279},[225,945,946],{"class":234},"    root.insertChildNode(",[225,948,46],{"class":290},[225,950,403],{"class":234},[225,952,953],{"class":157,"line":307},[225,954,955],{"class":234},"    node.parent().removeChildNode(node)\n",[225,957,958],{"class":157,"line":327},[225,959,248],{"emptyLinePlaceholder":247},[225,961,962],{"class":157,"line":332},[225,963,964],{"class":234},"move_to_top(project.layerTreeRoot(), annotation_layer.id())\n",[14,966,967,969,970,972],{},[191,968,414],{}," The same clone-and-remove idiom moves a node anywhere, including out of a group and up to the root. A useful convention for generated projects is to build the tree top-down in the order you want it drawn — annotation, points, lines, polygons, raster, base map — because inserting each new group at index ",[207,971,46],{}," as you go leaves the structure correct without any later reordering. Where scripts get this wrong the symptom is characteristic: everything renders, and the most important layer is underneath.",[14,974,975],{},[29,976,979,982,985,988,995,998,1003,1007,1012,1017,1021,1025,1027,1030,1032,1035,1038,1042,1047,1051,1053,1056,1058,1061,1064,1068,1074],{"viewBox":977,"role":32,"ariaLabel":978,"xmlns":34},"0 0 760 258","Diagram showing panel order against draw order, with the first child of the tree drawn last and therefore on top of the map",[36,980,981],{},"Panel order is draw order, upside down",[40,983,984],{},"The layer at the top of the panel is drawn last and therefore appears above everything else on the map. Building a tree top down in the order annotation, points, lines, polygons, raster and base map produces the correct stacking, while adding each new layer at the top of the tree as it is created reverses it.",[44,986],{"x":46,"y":46,"width":47,"height":987,"fill":49},"258",[51,989,990],{},[54,991,993],{"id":992,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"orderArrow",[63,994],{"d":65,"fill":83},[68,996,997],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Top of the panel means last to draw",[44,999],{"x":1000,"y":79,"width":1001,"height":1002,"rx":597,"fill":107,"stroke":66,"style":97},"40","280","184",[68,1004,1006],{"x":80,"y":1005,"style":101,"fill":73,"textAnchor":74},"76","the Layers panel",[44,1008],{"x":610,"y":1009,"width":1010,"height":1011,"rx":59,"fill":82,"stroke":83,"style":97},"90","236","26",[68,1013,1016],{"x":1014,"y":1015,"style":611,"fill":113},"78","108","Annotation — index 0",[44,1018],{"x":610,"y":1019,"width":1010,"height":1011,"rx":59,"fill":107,"stroke":66,"style":1020},"120","stroke-width:1.2",[68,1022,1024],{"x":1014,"y":1023,"style":611,"fill":113},"138","Survey points",[44,1026],{"x":610,"y":126,"width":1010,"height":1011,"rx":59,"fill":107,"stroke":66,"style":1020},[68,1028,1029],{"x":1014,"y":596,"style":611,"fill":113},"Parcels",[44,1031],{"x":610,"y":80,"width":1010,"height":1011,"rx":59,"fill":107,"stroke":66,"style":1020},[68,1033,1034],{"x":1014,"y":144,"style":611,"fill":113},"Base map — last index",[44,1036],{"x":1037,"y":79,"width":1001,"height":1002,"rx":597,"fill":95,"stroke":96,"style":97},"440",[68,1039,1041],{"x":1040,"y":1005,"style":101,"fill":96,"textAnchor":74},"580","the rendered map",[44,1043],{"x":1044,"y":80,"width":174,"height":1011,"rx":1045,"fill":66,"fillOpacity":1046,"stroke":66,"style":108},"470","4",0.25,[68,1048,1050],{"x":168,"y":144,"style":1049,"fill":113},"font-size:10px;font-family:sans-serif","base map drawn first",[44,1052],{"x":1044,"y":126,"width":174,"height":1011,"rx":1045,"fill":146,"fillOpacity":1046,"stroke":146,"style":108},[68,1054,1055],{"x":168,"y":596,"style":1049,"fill":113},"parcels over it",[44,1057],{"x":1044,"y":1019,"width":174,"height":1011,"rx":1045,"fill":96,"fillOpacity":1046,"stroke":96,"style":108},[68,1059,1060],{"x":168,"y":1023,"style":1049,"fill":113},"points over those",[44,1062],{"x":1044,"y":1009,"width":174,"height":1011,"rx":1045,"fill":83,"fillOpacity":1063,"stroke":83,"style":97},0.3,[68,1065,1067],{"x":168,"y":1015,"style":1066,"fill":113},"font-size:10px;font-weight:bold;font-family:sans-serif","annotation on top",[157,1069],{"x1":1070,"y1":1071,"x2":1072,"y2":1071,"stroke":83,"style":1073},"320","103","434","stroke-width:2;marker-end:url(#orderArrow)",[157,1075],{"x1":1070,"y1":1076,"x2":1072,"y2":1076,"stroke":83,"style":1073},"193",[180,1078,1080],{"id":1079},"build-a-whole-structure-in-one-pass","Build a whole structure in one pass",[14,1082,1083],{},"Generated projects benefit from a small helper that takes a description of the structure and applies it, rather than a long script of individual calls that is hard to review.",[216,1085,1087],{"className":218,"code":1086,"language":220,"meta":221,"style":221},"STRUCTURE = {\n    \"Annotation\": [\"Labels\", \"Notes\"],\n    \"Analysis\": [\"Flood extent\", \"Affected parcels\"],\n    \"Base map\": [\"Roads\", \"Buildings\", \"Terrain\"],\n}\n\ndef build_tree(project, structure):\n    root = project.layerTreeRoot()\n    for group_name, layer_names in structure.items():\n        group = root.findGroup(group_name) or root.addGroup(group_name)\n        for layer_name in layer_names:\n            for layer in project.mapLayersByName(layer_name):\n                existing = root.findLayer(layer.id())\n                if existing:\n                    group.insertChildNode(-1, existing.clone())\n                    existing.parent().removeChildNode(existing)\n                else:\n                    group.addLayer(layer)\n    return root\n\nbuild_tree(QgsProject.instance(), STRUCTURE)\n",[207,1088,1089,1100,1119,1136,1156,1161,1165,1175,1184,1196,1212,1225,1237,1246,1254,1269,1275,1284,1290,1299,1304],{"__ignoreMap":221},[225,1090,1091,1094,1097],{"class":157,"line":227},[225,1092,1093],{"class":290},"STRUCTURE",[225,1095,1096],{"class":230}," =",[225,1098,1099],{"class":234}," {\n",[225,1101,1102,1105,1108,1111,1113,1116],{"class":157,"line":244},[225,1103,1104],{"class":296},"    \"Annotation\"",[225,1106,1107],{"class":234},": [",[225,1109,1110],{"class":296},"\"Labels\"",[225,1112,293],{"class":234},[225,1114,1115],{"class":296},"\"Notes\"",[225,1117,1118],{"class":234},"],\n",[225,1120,1121,1124,1126,1129,1131,1134],{"class":157,"line":251},[225,1122,1123],{"class":296},"    \"Analysis\"",[225,1125,1107],{"class":234},[225,1127,1128],{"class":296},"\"Flood extent\"",[225,1130,293],{"class":234},[225,1132,1133],{"class":296},"\"Affected parcels\"",[225,1135,1118],{"class":234},[225,1137,1138,1141,1143,1145,1147,1149,1151,1154],{"class":157,"line":263},[225,1139,1140],{"class":296},"    \"Base map\"",[225,1142,1107],{"class":234},[225,1144,347],{"class":296},[225,1146,293],{"class":234},[225,1148,358],{"class":296},[225,1150,293],{"class":234},[225,1152,1153],{"class":296},"\"Terrain\"",[225,1155,1118],{"class":234},[225,1157,1158],{"class":157,"line":274},[225,1159,1160],{"class":234},"}\n",[225,1162,1163],{"class":157,"line":279},[225,1164,248],{"emptyLinePlaceholder":247},[225,1166,1167,1169,1172],{"class":157,"line":307},[225,1168,679],{"class":230},[225,1170,1171],{"class":682}," build_tree",[225,1173,1174],{"class":234},"(project, structure):\n",[225,1176,1177,1180,1182],{"class":157,"line":327},[225,1178,1179],{"class":234},"    root ",[225,1181,257],{"class":230},[225,1183,271],{"class":234},[225,1185,1186,1188,1191,1193],{"class":157,"line":332},[225,1187,713],{"class":230},[225,1189,1190],{"class":234}," group_name, layer_names ",[225,1192,341],{"class":230},[225,1194,1195],{"class":234}," structure.items():\n",[225,1197,1198,1201,1203,1206,1209],{"class":157,"line":364},[225,1199,1200],{"class":234},"        group ",[225,1202,257],{"class":230},[225,1204,1205],{"class":234}," root.findGroup(group_name) ",[225,1207,1208],{"class":230},"or",[225,1210,1211],{"class":234}," root.addGroup(group_name)\n",[225,1213,1214,1217,1220,1222],{"class":157,"line":375},[225,1215,1216],{"class":230},"        for",[225,1218,1219],{"class":234}," layer_name ",[225,1221,341],{"class":230},[225,1223,1224],{"class":234}," layer_names:\n",[225,1226,1227,1230,1232,1234],{"class":157,"line":384},[225,1228,1229],{"class":230},"            for",[225,1231,338],{"class":234},[225,1233,341],{"class":230},[225,1235,1236],{"class":234}," project.mapLayersByName(layer_name):\n",[225,1238,1239,1242,1244],{"class":157,"line":395},[225,1240,1241],{"class":234},"                existing ",[225,1243,257],{"class":230},[225,1245,372],{"class":234},[225,1247,1248,1251],{"class":157,"line":406},[225,1249,1250],{"class":230},"                if",[225,1252,1253],{"class":234}," existing:\n",[225,1255,1257,1260,1263,1266],{"class":157,"line":1256},15,[225,1258,1259],{"class":234},"                    group.insertChildNode(",[225,1261,1262],{"class":230},"-",[225,1264,1265],{"class":290},"1",[225,1267,1268],{"class":234},", existing.clone())\n",[225,1270,1272],{"class":157,"line":1271},16,[225,1273,1274],{"class":234},"                    existing.parent().removeChildNode(existing)\n",[225,1276,1278,1281],{"class":157,"line":1277},17,[225,1279,1280],{"class":230},"                else",[225,1282,1283],{"class":234},":\n",[225,1285,1287],{"class":157,"line":1286},18,[225,1288,1289],{"class":234},"                    group.addLayer(layer)\n",[225,1291,1293,1296],{"class":157,"line":1292},19,[225,1294,1295],{"class":230},"    return",[225,1297,1298],{"class":234}," root\n",[225,1300,1302],{"class":157,"line":1301},20,[225,1303,248],{"emptyLinePlaceholder":247},[225,1305,1307,1310,1312],{"class":157,"line":1306},21,[225,1308,1309],{"class":234},"build_tree(QgsProject.instance(), ",[225,1311,1093],{"class":290},[225,1313,450],{"class":234},[14,1315,1316,1318,1319,1322],{},[191,1317,414],{}," Because Python dictionaries preserve insertion order, the groups are created top to bottom in the order written — annotation first, base map last, which is the draw order a map wants. ",[207,1320,1321],{},"insertChildNode(-1, ...)"," appends to the end of the group, so layers keep the order they appear in the list. Handling both the \"already in the tree\" and \"registered but not shown\" cases in one function means the same helper works whether the layers were added by the script or were already in a template project. Keeping the structure as data rather than code also makes it reviewable: a colleague can check the map's organisation without reading any Python.",[180,1324,1326],{"id":1325},"qgis-version-compatibility","QGIS version compatibility",[1328,1329,1330,1346],"table",{},[1331,1332,1333],"thead",{},[1334,1335,1336,1340,1343],"tr",{},[1337,1338,1339],"th",{},"QGIS version",[1337,1341,1342],{},"Python",[1337,1344,1345],{},"Notes",[1347,1348,1349,1361,1371,1382],"tbody",{},[1334,1350,1351,1355,1358],{},[1352,1353,1354],"td",{},"3.22 LTR",[1352,1356,1357],{},"3.9",[1352,1359,1360],{},"Full node API as described, including mutually exclusive groups.",[1334,1362,1363,1366,1368],{},[1352,1364,1365],{},"3.28 LTR",[1352,1367,1357],{},[1352,1369,1370],{},"Identical.",[1334,1372,1373,1376,1379],{},[1352,1374,1375],{},"3.34 LTR",[1352,1377,1378],{},"3.12",[1352,1380,1381],{},"Baseline for this page.",[1334,1383,1384,1387,1389],{},[1352,1385,1386],{},"3.40 \u002F 3.44",[1352,1388,1378],{},[1352,1390,1391],{},"Identical; the panel gained filtering and layer-tree search, neither of which changes these calls.",[14,1393,1394,1397,1398,1401,1402,1405],{},[207,1395,1396],{},"setItemVisibilityChecked()"," replaced the older ",[207,1399,1400],{},"setVisible()"," on tree nodes in QGIS 3.0; snippets using ",[207,1403,1404],{},"setVisible(Qt.Checked)"," are QGIS 2 code and will fail on any 3.x release.",[180,1407,1409],{"id":1408},"troubleshooting","Troubleshooting",[185,1411,1412,1428,1434,1443,1453,1463],{},[188,1413,1414,1423,1424,1427],{},[191,1415,1416,1419,1420,1422],{},[207,1417,1418],{},"findGroup()"," returns ",[207,1421,878],{}," for a group you can see."," The name must match exactly, including case and any trailing space. Use ",[207,1425,1426],{},"root.findGroups()"," and print the names when in doubt.",[188,1429,1430,1433],{},[191,1431,1432],{},"A layer vanished after moving it."," The original node was removed before the clone was inserted, or the clone was inserted into a node that was itself then removed. Insert first, remove second.",[188,1435,1436,415,1439,1442],{},[191,1437,1438],{},"Unchecking a group did not hide its layers.",[207,1440,1441],{},"setItemVisibilityChecked(False)"," on the group hides the group's contents on the canvas, but a per-layer check state is preserved underneath; use the recursive form if you want the children cleared too.",[188,1444,1445,1448,1449,1452],{},[191,1446,1447],{},"The panel order does not match what the script built."," Check whether layers were added with ",[207,1450,1451],{},"addMapLayer(layer)"," — that always inserts at the root, on top of your carefully built structure.",[188,1454,1455,1458,1459,1462],{},[191,1456,1457],{},"A group looks empty but the layers are still in the project."," Removing a tree node hides a layer without unregistering it. Remove the layer itself with ",[207,1460,1461],{},"removeMapLayer()"," if that is what you meant.",[188,1464,1465,1468],{},[191,1466,1467],{},"Nothing renders after a restructure."," A mutually exclusive group with no checked child renders nothing at all. Check one explicitly after building it.",[180,1470,1472],{"id":1471},"conclusion","Conclusion",[14,1474,1475,1476,1479,1480,1483,1484,1486],{},"The layer tree is a separate structure from the layer registry, built from group and layer nodes. Create groups with ",[207,1477,1478],{},"addGroup()"," or ",[207,1481,1482],{},"insertGroup()",", place new layers with ",[207,1485,467],{}," after registering them without a legend entry, and move existing ones by cloning the node and removing the original. Set expansion, visibility and exclusivity deliberately — they are stored in the project and are most of what makes a generated project feel hand-made.",[180,1488,1490],{"id":1489},"frequently-asked-questions","Frequently Asked Questions",[14,1492,1493,1496],{},[191,1494,1495],{},"How do I move a layer into a group without losing its style?","\nClone the tree node and remove the original. The style belongs to the map layer, not the node, so it is never affected by tree operations.",[14,1498,1499,1502,1503,1505],{},[191,1500,1501],{},"Can a group contain another group?","\nYes, to any depth. ",[207,1504,1478],{}," on a group node creates a subgroup, and the recursion in the traversal example handles arbitrary nesting.",[14,1507,1508,1511],{},[191,1509,1510],{},"What is the difference between a layer node and a map layer?","\nThe map layer holds the data, style and id; the tree node is a pointer to it with panel state such as check status and expansion. One map layer has at most one node in a project.",[14,1513,1514,1517],{},[191,1515,1516],{},"How do I hide a layer from the panel but keep it rendering?","\nYou cannot — rendering follows the tree. What you can do is register the layer without a node so it is neither shown nor drawn, and use it purely as a data source for Processing.",[14,1519,1520,1523],{},[191,1521,1522],{},"Do groups affect performance?","\nNo. Grouping is presentation only; the renderer draws the same layers either way.",[180,1525,1527],{"id":1526},"related","Related",[185,1529,1530,1535,1539,1545,1551],{},[188,1531,1532,1534],{},[21,1533,24],{"href":23}," — the guide this recipe belongs to",[188,1536,1537],{},[21,1538,201],{"href":200},[188,1540,1541],{},[21,1542,1544],{"href":1543},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fsave-and-load-qgis-project-pyqgis\u002F","Save and Load a QGIS Project in PyQGIS",[188,1546,1547],{},[21,1548,1550],{"href":1549},"\u002Fspatial-data-processing-automation\u002Fautomated-map-layout-generation\u002Fadd-legend-to-layout-pyqgis\u002F","Add a Legend to a Layout in PyQGIS",[188,1552,1553],{},[21,1554,1556],{"href":1555},"\u002Fpyqgis-cartography-visualization\u002Fprogrammatic-layer-styling\u002F","Programmatic Layer Styling in PyQGIS",[1558,1559,1560],"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 pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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}",{"title":221,"searchDepth":244,"depth":244,"links":1562},[1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573],{"id":182,"depth":244,"text":183},{"id":213,"depth":244,"text":214},{"id":478,"depth":244,"text":479},{"id":650,"depth":244,"text":651},{"id":890,"depth":244,"text":891},{"id":1079,"depth":244,"text":1080},{"id":1325,"depth":244,"text":1326},{"id":1408,"depth":244,"text":1409},{"id":1471,"depth":244,"text":1472},{"id":1489,"depth":244,"text":1490},{"id":1526,"depth":244,"text":1527},"Build, reorder and control the QGIS Layers panel from Python — creating groups, moving layers between them, setting visibility and mutually exclusive groups, and cloning nodes without losing styling.","md",{"slug":1577,"type":1578,"breadcrumb":1579,"datePublished":1580,"dateModified":1580},"organise-layer-tree-groups-pyqgis","article","Layer Tree Groups","2026-08-15","\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis",{"title":5,"description":1574},"pyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Forganise-layer-tree-groups-pyqgis\u002Findex","e2EADJQnZ3Cr6iM06BPwzPtxjmMGbiTGAVmT6j_zudU",1786789584634]