Access the Active Layer and Selection in PyQGIS
Interactive PyQGIS almost always starts the same way: the user has clicked a layer, maybe selected a few features, and wants a script to act on exactly that. Three or four calls cover it — and each has a failure mode that produces a confusing error rather than a helpful one, because "no layer is active" and "the active layer is a raster" both arrive as an AttributeError several lines later.
This recipe belongs to QGIS Python Console Basics. It covers reading the active layer, working with the current selection, changing the selection from code, the checks that turn cryptic errors into clear ones, and how to keep the same logic usable outside the console.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer, with the Python console open.
- A project with at least one vector layer, and a few features selected to experiment with.
ifaceis already available in the console; nothing needs importing for it.
Get the active layer safely
from qgis.core import QgsVectorLayer, QgsWkbTypes
layer = iface.activeLayer()
if layer is None:
raise RuntimeError("No layer is active — click one in the Layers panel")
if not isinstance(layer, QgsVectorLayer):
raise RuntimeError(f"{layer.name()} is not a vector layer")
if layer.geometryType() != QgsWkbTypes.PolygonGeometry:
raise RuntimeError(f"{layer.name()} is not polygons")
print(layer.name(), layer.featureCount(), layer.crs().authid())
Breakdown: Three checks, three clear messages, and every later line can then assume it has a polygon vector layer. Without them, layer.getFeatures() on None raises an AttributeError about NoneType, which tells the user nothing about what they should have clicked. isinstance() rather than a type comparison keeps the check working for subclasses. The geometry check matters more than it looks: an algorithm expecting polygons will usually run on lines and produce silently wrong output rather than failing.
The active layer is the one highlighted in the Layers panel, which is not the same as the layers that are checked, nor the same as the layers the user has selected in the panel. For the latter:
selected_layers = iface.layerTreeView().selectedLayers()
print([lyr.name() for lyr in selected_layers])
Breakdown: selectedLayers() returns every layer highlighted in the panel, which is what you want for a tool that acts on several at once. It returns an empty list when nothing is highlighted, so a plain if not selected_layers: check is the whole error handling. Note that a group node being highlighted does not put its children in this list — collect those from the tree if your tool should treat a group as a selection.
Work with the selected features
count = layer.selectedFeatureCount()
print(f"{count} selected")
if count == 0:
features = layer.getFeatures() # act on everything
else:
features = layer.selectedFeatures() # act on the selection
total_area = sum(feature.geometry().area() for feature in features)
print(round(total_area, 2))
Breakdown: selectedFeatureCount() is cheap and does not fetch anything, so it is the right thing to branch on; selectedFeatures() builds the full feature objects and is proportionally expensive on a large selection. The fall-back-to-everything pattern shown here is the convention users expect from QGIS's own tools — an empty selection means "the whole layer", not "nothing" — and matching that convention makes a script feel native. For ids alone, layer.selectedFeatureIds() is far cheaper and is all you need when the next step is a feature request.
For anything more than a few hundred features, pass the ids into a request rather than materialising them all:
from qgis.core import QgsFeatureRequest
request = QgsFeatureRequest().setFilterFids(layer.selectedFeatureIds())
for feature in layer.getFeatures(request):
...
Breakdown: This streams the selected features one at a time instead of building a list of all of them, which matters when a user has selected fifty thousand parcels. It also composes with the other request options — an attribute subset, no geometry — described in Speed Up Feature Iteration with QgsFeatureRequest.
Change the selection from code
layer.selectByExpression("\"risk_band\" = 'high' AND \"area_m2\" > 5000")
print(layer.selectedFeatureCount())
layer.selectByIds([12, 47, 108], QgsVectorLayer.AddToSelection)
layer.removeSelection()
Breakdown: selectByExpression() takes QGIS expression syntax, with field names in double quotes and string literals in single quotes — the opposite of what most people type first. selectByIds() takes a list of feature ids and an optional behaviour flag: SetSelection replaces, AddToSelection adds, IntersectSelection narrows, RemoveFromSelection subtracts, which together let a script build a selection in steps. removeSelection() clears it. Selecting from a script is often more useful than acting on the selection directly, because it leaves the user in control of what happens next — and it makes the script's effect visible on the map. The expression language itself is covered in Select Features by Expression in PyQGIS.
To show the user what was selected, zoom to it:
iface.mapCanvas().zoomToSelected(layer)
iface.mapCanvas().refresh()
Breakdown: zoomToSelected() sets the canvas extent to the selection's bounding box with a small margin, and does nothing when nothing is selected — no error, no movement, which is worth knowing when a script appears to have ignored the call. The explicit refresh() is occasionally needed when several canvas changes are made in quick succession from a script.
Keep the logic usable outside the console
Everything above except iface works in any context, and that suggests the shape a console experiment should take before it becomes something you run regularly.
def summarise(layer, feature_ids=None):
request = QgsFeatureRequest()
if feature_ids:
request.setFilterFids(list(feature_ids))
return sum(f.geometry().area() for f in layer.getFeatures(request))
# in the console
print(summarise(iface.activeLayer(), iface.activeLayer().selectedFeatureIds()))
# in a scheduled script
print(summarise(QgsProject.instance().mapLayersByName("Parcels")[0]))
Breakdown: The function takes what it needs as arguments and never mentions iface, so the same code serves an interactive experiment and an unattended run. This is the small discipline that saves rewriting a script when somebody asks for it nightly, and it is the same separation described in QGIS Core, GUI and Analysis Modules Explained. Passing None for the ids meaning "everything" keeps the call sites short.
QGIS version compatibility
| QGIS version | Python | Notes |
|---|---|---|
| 3.22 LTR | 3.9 | All calls as described. |
| 3.28 LTR | 3.9 | Identical. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Identical; selection behaviour flags gained scoped enumeration names alongside the legacy ones. |
layer.selectedFeaturesIterator() from older examples was removed in the 3.x series; use a feature request filtered by the selected ids, as above.
Troubleshooting
AttributeError: 'NoneType' object has no attribute ...No layer is active. Check forNonefirst and say so.- The script runs on the wrong layer. The active layer is the highlighted one, which changes as the user clicks. Print
layer.name()before acting. selectByExpression()selects nothing. Quoting: field names take double quotes, string values single. Test the same expression in the layer's filter dialog.- The selection looks unchanged on the map. The canvas has not refreshed, or the selection was set on a different layer object than the one displayed.
selectedFeatures()is slow. It builds every feature. Use the ids with a request instead.ifaceis not defined. You are not in the QGIS console. Pass the layer in explicitly.
Conclusion
Read the active layer with iface.activeLayer() and validate it before use — that one habit turns most confusing errors into clear messages. Branch on selectedFeatureCount(), treat an empty selection as "the whole layer" the way QGIS's own tools do, and prefer selected ids with a feature request over materialising every selected feature. Keep the actual work in a function that takes a layer and some ids, and the console experiment becomes a scheduled job without a rewrite.
Frequently Asked Questions
What is the difference between the active layer and a selected layer? The active layer is the single one QGIS considers current; selected layers are everything highlighted in the panel. Tools acting on one use the first, tools acting on several use the second.
Does the selection persist when the project is saved? No. Selection is transient interface state. Store the feature ids yourself if a workflow needs to resume where it left off.
Can I select features on a layer that is not visible? Yes. Selection is a property of the layer, independent of its check state or the canvas.
How do I select features that intersect another layer?
Run the native:selectbylocation algorithm, or build the ids with a spatial index and pass them to selectByIds() — see Build a Spatial Index in PyQGIS.
Why does zoomToSelected() do nothing?
Nothing is selected on that layer, or the layer passed is not the one with the selection. It fails silently by design.
How do I react when the user changes the selection?
Connect to the layer's selectionChanged signal, which passes the added and removed feature ids. It is the right hook for a dock widget that shows a live summary of what is selected, and it must be disconnected when the widget closes or the handler outlives the panel.
Can I copy the selected features to a new layer?
Yes — run native:saveselectedfeatures through Processing, which takes the layer and writes only its selection to the output. Doing it by hand with a memory layer works too, but the algorithm handles the fields, the coordinate system and the geometry type without any of the usual mismatches.