Select Features by Expression in PyQGIS
Selecting features by attribute is the scripted equivalent of the "Select by Expression" dialog, and it is the fastest way to turn a rule into a visible, actionable set of rows on the map. selectByExpression() does it in one line — but a selection is a piece of interface state, and treating it as a data-processing tool is where scripts go wrong: selections do not survive a project reload, they interact with what the user has clicked, and iterating them is not the same as filtering.
This page is a focused recipe within Working with QGIS Expressions in PyQGIS. It covers making a selection, combining it with an existing one, reading it back, deciding when a selection is the wrong tool entirely, and feeding a selection into Processing algorithms.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer.
- A vector layer loaded in the project. Selections only exist on layers registered with
QgsProject; a layer opened but never added has nothing to select on. - Familiarity with expression quoting — double quotes for fields, single for literals. See Evaluate a QGIS Expression in PyQGIS.
Make a selection
The one-line form replaces whatever was selected before and highlights the matches on the canvas immediately.
from qgis.core import QgsProject
layer = QgsProject.instance().mapLayersByName("parcels")[0]
layer.selectByExpression('"zoning" = \'residential\' AND "assessed_value" > 250000')
print(layer.selectedFeatureCount(), "feature(s) selected")
Breakdown: The expression is an ordinary Python string, so the single quotes that delimit the literal residential in the expression have to be escaped — using a double-quoted Python string with single quotes inside, or a triple-quoted string, avoids the backslashes entirely. selectedFeatureCount() is the cheap way to confirm a match without pulling the features themselves; a count of zero almost always means a quoting mistake rather than genuinely empty data.
For values that come from anywhere other than a literal in your source, build the expression with the quoting helpers rather than an f-string:
from qgis.core import QgsExpression
zoning = user_input # e.g. "O'Brien Estate"
clause = f'{QgsExpression.quotedColumnRef("zoning")} = {QgsExpression.quotedValue(zoning)}'
layer.selectByExpression(clause)
Breakdown: quotedValue() doubles internal apostrophes the way the expression grammar requires, so a name containing one selects correctly instead of producing a parser error. quotedColumnRef() does the same for the field name. This matters in any plugin where the value originates in a dialog.
Combine with an existing selection
The optional second argument controls how the new match interacts with what is already selected, which is what makes multi-step refinement possible.
from qgis.core import Qgis, QgsProject
layer = QgsProject.instance().mapLayersByName("parcels")[0]
layer.selectByExpression('"zoning" = \'residential\'', Qgis.SelectBehavior.SetSelection)
layer.selectByExpression('"assessed_value" > 500000', Qgis.SelectBehavior.IntersectSelection)
layer.selectByExpression('"status" = \'exempt\'', Qgis.SelectBehavior.RemoveFromSelection)
print(layer.selectedFeatureCount())
Breakdown: Reading down, this selects residential parcels, narrows to the expensive ones, then drops the exempt ones — three simple expressions instead of one long boolean. SetSelection is the default and is what makes the first call a clean starting point regardless of previous state. On QGIS 3.28 and earlier these constants live on QgsVectorLayer (QgsVectorLayer.SetSelection); both spellings work on 3.34, and the Qgis.SelectBehavior form is the one to write in new code.
Read the selection back
A selection is a set of feature IDs, and there are two ways to consume it depending on whether you need the features or just the identifiers.
selected_ids = layer.selectedFeatureIds()
print(len(selected_ids), "id(s)")
for feature in layer.getSelectedFeatures():
print(feature["parcel_id"], feature["assessed_value"])
total = sum(f["assessed_value"] or 0 for f in layer.getSelectedFeatures())
print(f"total assessed value: {total:,.0f}")
Breakdown: selectedFeatureIds() returns just the IDs and is nearly free — use it when you only need a count, a membership test, or something to pass to another call. getSelectedFeatures() returns an iterator of full features and reads from the provider, so it costs the same as any other iteration. The or 0 guard handles NULL values, which would otherwise make the sum() raise on the first empty cell.
When a selection is the wrong tool
A selection is user-interface state. It is visible, it is what the user sees highlighted, and it does not persist. For pure data processing — reading rows that match a rule — a feature request is faster, does not disturb what the user has selected, and works on layers that are not in the project at all.
from qgis.core import QgsFeatureRequest
request = QgsFeatureRequest()
request.setFilterExpression('"zoning" = \'residential\' AND "assessed_value" > 250000')
request.setSubsetOfAttributes(["parcel_id", "assessed_value"], layer.fields())
for feature in layer.getFeatures(request):
print(feature["parcel_id"], feature["assessed_value"])
Breakdown: The filter is translated into SQL for providers that support it — PostGIS and GeoPackage among them — so the database does the matching and only the matching rows are transferred. selectByExpression(), by contrast, must evaluate the expression against every feature to decide what to highlight. Adding setSubsetOfAttributes() narrows the columns as well. Use a selection when a human needs to see the result; use a request when only the script does.
Feed a selection into Processing
When a selection is the right starting point — because the user made it — Processing algorithms can operate on just those features rather than the whole layer.
import processing
from qgis.core import QgsProcessingFeatureSourceDefinition
source = QgsProcessingFeatureSourceDefinition(layer.id(), selectedFeaturesOnly=True)
result = processing.run("native:buffer", {
"INPUT": source,
"DISTANCE": 50,
"OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]
print(result.featureCount(), "buffered feature(s)")
Breakdown: QgsProcessingFeatureSourceDefinition wraps a layer ID with the selectedFeaturesOnly flag, which is how the GUI's "Selected features only" checkbox is expressed in a script. Passing the layer object directly instead would silently process everything. This composes with the pipelines in Chaining Processing Algorithms in PyQGIS, where a user-driven selection often defines the study area for a whole chain.
Select across several layers at once
A study area is rarely one layer. Applying the same rule across every layer that has the relevant field — and reporting which ones did not — turns a repetitive manual job into four lines.
from qgis.core import QgsProject
RULE = '"status" = \'active\''
report = {}
for layer in QgsProject.instance().mapLayers().values():
if layer.type() != layer.VectorLayer:
continue
if layer.fields().indexOf("status") == -1:
report[layer.name()] = "no status field"
continue
layer.selectByExpression(RULE)
report[layer.name()] = f"{layer.selectedFeatureCount()} selected"
for name, outcome in sorted(report.items()):
print(f"{name:<28} {outcome}")
Breakdown: mapLayers() returns every layer in the project keyed by ID, including rasters, so the type check comes first — calling selectByExpression() on a raster raises. Testing indexOf("status") == -1 before selecting distinguishes "no matching rows" from "this layer does not have that column", which are very different findings and look identical in a bare count. Building a report dictionary rather than printing inside the loop keeps the output ordered and makes the whole thing easy to return from a function.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | Use QgsVectorLayer.SetSelection etc. rather than Qgis.SelectBehavior. |
| 3.34 LTR | 3.12 | Baseline. Both enum spellings accepted. |
| 3.40 / 3.44 | 3.12 | Qgis.SelectBehavior is the documented form; the old constants still resolve. |
selectByExpression(), selectedFeatureIds() and getSelectedFeatures() are unchanged across the whole 3.x line.
Troubleshooting
- Zero features selected but the data clearly matches. A quoting error.
"zoning" = "residential"compares two field references;'zoning' = 'residential'compares two literals and is always false. Only the mixed form is right. - The selection is empty after a Processing run. Some algorithms replace the layer rather than editing it. Re-select on the output layer, not the input.
selectByExpressionraises about an unknown field. The field name has a typo or the layer is not the one you think it is. Printlayer.fields().names()andlayer.name()together.- The selection does not appear on the canvas. The layer is not registered with the project, or its visibility is off. Selections are drawn as part of the layer's render.
- The user's selection disappears when your script runs. You used
SetSelectionwhereAddToSelectionwas meant, or you should not be selecting at all — use a feature request instead. - A Processing algorithm ignores the selection. The input was passed as a layer object. Wrap it in
QgsProcessingFeatureSourceDefinition(layer.id(), selectedFeaturesOnly=True).
Conclusion
selectByExpression() is the right tool when a person needs to see the result: it is one line, it composes through the four selection behaviours, and it feeds Processing cleanly through QgsProcessingFeatureSourceDefinition. When only the script consumes the rows, reach for a QgsFeatureRequest instead — it is faster, it pushes work to the provider, and it leaves the user's own selection alone.
Frequently Asked Questions
Why does my selection expression match nothing?
Almost always quoting. Field names take double quotes and string values take single quotes; getting either wrong produces a valid expression that is never true rather than an error. Verify with layer.selectedFeatureCount() immediately after the call.
How do I add to a selection instead of replacing it?
Pass Qgis.SelectBehavior.AddToSelection as the second argument. SetSelection is the default, which is why a second call normally appears to discard the first result.
Is selecting features faster than filtering them?
No — it is slower for pure data work. A selection evaluates the expression against every feature and updates interface state. A QgsFeatureRequest with setFilterExpression() lets the provider do the matching and returns only the rows you want.
Does a selection persist when the project is saved? No. Selections are transient interface state and are empty again after a reload. If a set of features needs to persist, write a flag to an attribute or export them to their own layer.