Toggle Layer Visibility and Legend Entries in PyQGIS
Turning a layer on and off is the first thing anybody automates in QGIS, and the first thing that surprises them, because QgsVectorLayer has no visibility property at all. Visibility belongs to the tree of nodes that represents the project in the Layers panel — and once you know that, a whole family of controls opens up: group checkboxes, mutually exclusive groups, per-class legend ticks, and the difference between "checked" and "actually drawn".
This recipe belongs to Map Themes & Layer Visibility in PyQGIS. It covers finding a node, toggling layers and groups, filtering a categorized layer through its legend, and batching many changes so the canvas redraws once instead of forty times.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A project with at least one group, so the ancestor behaviour can be observed.
- For the legend section, a layer using a categorized or rule-based renderer; see graduated and categorized renderers.
Find the node, then toggle it
Every operation starts by turning a layer into a tree node.
from qgis.core import QgsProject
project = QgsProject.instance()
root = project.layerTreeRoot()
layer = project.mapLayersByName("flood extent")[0]
node = root.findLayer(layer.id())
node.setItemVisibilityChecked(True)
print(node.itemVisibilityChecked(), node.isVisible())
Breakdown: findLayer() searches the whole tree recursively, so a layer nested three groups deep is found without walking anything yourself. It returns None for a layer that is in the project's registry but not in the tree — which happens when a layer was added with addMapLayer(layer, False) — so a None check belongs in any function that takes a layer name from outside. The two accessors answer different questions: itemVisibilityChecked() reports this node's own box, isVisible() walks up through every parent group and reports whether the layer will actually be drawn.
Toggling rather than setting is a one-liner, and useful for a plugin's toolbar button:
node.setItemVisibilityChecked(not node.itemVisibilityChecked())
Breakdown: No repaint call is needed anywhere in this section. The canvas subscribes to the tree's change signals and refreshes itself, which is also why a loop that flips forty layers individually triggers forty redraws.
Groups, and exclusive groups
A group node has the same visibility API plus one extra behaviour worth knowing.
group = root.findGroup("Analysis")
group.setItemVisibilityChecked(True)
group.setItemVisibilityCheckedRecursive(True)
group.setIsMutuallyExclusive(True, initialChildIndex=0)
Breakdown: setItemVisibilityChecked() on a group ticks the group itself and leaves the children alone; setItemVisibilityCheckedRecursive() ticks the group and everything beneath it, which is the call people usually mean. setIsMutuallyExclusive() turns the group into a radio-button set: checking one child unchecks its siblings automatically, and QGIS enforces it thereafter. That single flag replaces a lot of hand-written toggle logic for scenario layers — 1-in-100, 1-in-200, 1-in-1000 flood extents in one exclusive group behave correctly no matter how the user clicks.
Finding a group by name only searches the immediate children unless you ask for recursion:
def find_group(root, name):
for child in root.children():
if child.nodeType() == child.NodeGroup:
if child.name() == name:
return child
found = find_group(child, name)
if found:
return found
return None
Breakdown: root.findGroup(name) does search recursively in current versions, but writing the walk explicitly is worth it when you need the path to a group rather than the group itself, or when duplicate group names make the first match the wrong one. nodeType() distinguishes group nodes from layer nodes, and comparing against child.NodeGroup avoids importing the enum separately.
Tick and untick legend classes
Below each layer node sit legend nodes — one per category, class or rule. Their checkboxes filter the map, not just the legend.
from qgis.core import QgsLayerTreeModelLegendNode
from qgis.PyQt.QtCore import Qt
model = iface.layerTreeView().layerTreeModel()
node = root.findLayer(layer.id())
for legend_node in model.layerLegendNodes(node):
label = legend_node.data(Qt.DisplayRole)
keep = label in {"flooding", "subsidence"}
legend_node.setData(
Qt.Checked if keep else Qt.Unchecked,
Qt.CheckStateRole,
)
Breakdown: Legend nodes follow Qt's model conventions rather than having named getters, so the label comes from data(Qt.DisplayRole) and the tick from setData(..., Qt.CheckStateRole). Only renderers that support legend filtering respond — categorized, graduated and rule-based do; a single-symbol renderer has one node and unticking it hides the layer. Because this is model state rather than tree state, a headless script must build its own QgsLayerTreeModel with the ShowLegend flag, as described in creating a map theme.
Batch changes without a repaint storm
Flipping many nodes one at a time makes the canvas redraw after each change, which is slow and looks broken.
canvas = iface.mapCanvas()
canvas.setRenderFlag(False)
try:
for tree_layer in root.findLayers():
tree_layer.setItemVisibilityChecked(
tree_layer.layer().name() in wanted_names
)
finally:
canvas.setRenderFlag(True)
canvas.refresh()
Breakdown: setRenderFlag(False) suspends canvas rendering entirely; the tree still updates, the panel still shows the new ticks, and nothing is drawn until the flag returns. The try/finally is not defensive padding — an exception raised mid-loop with rendering suspended leaves QGIS looking frozen, and users reasonably conclude it has crashed. findLayers() returns every layer node in the tree, so this pattern sets every layer explicitly rather than only turning the wanted ones on, which is what makes it deterministic when run repeatedly.
Reordering while you are in there
Draw order and visibility are separate, but scripts that manage one usually end up managing the other, and the tree API for moving nodes has one sharp edge: a node cannot be reparented, only cloned and removed.
def move_layer_to_top(root, layer):
node = root.findLayer(layer.id())
if node is None:
return
clone = node.clone()
parent = node.parent()
root.insertChildNode(0, clone)
parent.removeChildNode(node)
Breakdown: clone() copies the node including its checkbox state and custom properties, so the layer arrives at the top exactly as it was. Inserting before removing matters: remove first and the layer is briefly absent from the tree, which some panels and plugins notice and react to. parent() is used rather than assuming the root, because the node may have been sitting inside a group.
The same pattern moves a layer into a group — insert the clone as a child of the group node instead of the root. Nothing about the layer itself changes; only which node points at it.
Solo, and other patterns worth having
Three small helpers cover most of what an interface ends up needing, and all of them are built from the primitives above.
def solo(root, layer):
"""Show only this layer, and every group needed to reach it."""
for node in root.findLayers():
node.setItemVisibilityChecked(node.layer().id() == layer.id())
node = root.findLayer(layer.id())
parent = node.parent()
while parent is not None and parent is not root:
parent.setItemVisibilityChecked(True)
parent = parent.parent()
def visible_layers(root):
"""Layers that will genuinely be drawn, in draw order."""
return [n.layer() for n in root.findLayers() if n.isVisible()]
Breakdown: The upward walk in solo() is the part that is easy to forget and the reason naive solo buttons show nothing: ticking the target is useless if it lives in a group somebody unchecked earlier. visible_layers() returns objects rather than names because that is what the rest of a script usually wants — feeding a map settings object for an off-screen render, for instance, where the list must match what the canvas shows.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | setItemVisibilityChecked, isVisible, setIsMutuallyExclusive all present. |
| 3.22 LTR | 3.9 | findLayers() and findGroup() search recursively. |
| 3.28 LTR | 3.9 | Legend node check state stable across renderer types. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40+ | 3.12 | Layer tree nodes expose per-node custom properties used by newer panel features. |
Troubleshooting
findLayer()returns None. The layer is in the registry but not in the tree. Add it withproject.addMapLayer(layer)or insert a node manually.- The layer is checked but nothing draws. An ancestor group is unchecked. Test
node.isVisible(), notitemVisibilityChecked(). - Ticking a group did not tick its children. Use
setItemVisibilityCheckedRecursive(True). - Legend ticks do nothing. The renderer does not support legend filtering, or the script is headless and the model was built without
ShowLegend. - The canvas flickers through forty redraws. Wrap the loop in
setRenderFlag(False)and refresh once at the end. - QGIS appears frozen after an error. The render flag was left off. Always restore it in a
finallyblock.
Conclusion
Visibility lives on tree nodes: find the node, set its checkbox, and read isVisible() when you need to know whether the layer will really be drawn. Use recursive setters on groups, setIsMutuallyExclusive() for scenario sets, legend node check states for per-class filtering, and suspend the render flag around any loop that changes more than a handful of layers.
Frequently Asked Questions
How do I hide everything except one layer?
Iterate root.findLayers() and set each checkbox to whether it is the one you want. Setting only the target true leaves whatever was checked before still checked, which is the usual cause of a "solo" button that does not solo.
Can I react when the user toggles a layer?
Yes — connect to root.visibilityChanged for tree-wide notification, or to a specific node's own signal. That is how a plugin keeps a panel in step with the Layers panel without polling; see connecting layer signals.
Is visibility saved with the project? Yes, the whole tree including checkbox state is written into the project file. It is also what a map theme snapshots.
Does hiding a layer stop it being queried? No. Identify, selection by expression and processing algorithms all work on hidden layers, because they read the layer rather than the tree. Only rendering consults the checkbox.