Select Features by Location in PyQGIS
"Which buildings are inside the flood zone?", "which roads cross the county boundary?", "which wells are not within any protected area?" — selection by location is the spatial counterpart to a WHERE clause, and it is often the first step of a larger job: select, then count, export, edit or style what was selected. QGIS ships two algorithms for it, one that changes a layer's selection and one that writes the matching features out, plus the geometry predicates underneath both for when you need something neither algorithm does.
This recipe belongs to Vector Data Manipulation in PyQGIS. It runs both algorithms, explains how the predicate codes map to real relationships, combines spatial selections with existing ones, and reproduces the selection in Python with a spatial index.
Prerequisites
- QGIS 3.40 LTR or newer, or the QGIS 4 series.
- Two vector layers. They do not need to share a CRS — the algorithms reproject the comparison layer on the fly — but results are easier to check when they do.
- Valid geometries in both layers. An invalid polygon can make predicates return nothing; see finding and fixing invalid geometries.
Select features that meet a predicate
native:selectbylocation changes the selection on the input layer in place. It writes nothing to disk, which suits interactive work and scripts that go on to act on the selection.
import processing
from qgis.core import QgsProject
buildings = QgsProject.instance().mapLayersByName("buildings")[0]
flood = QgsProject.instance().mapLayersByName("flood_zone_3")[0]
INTERSECT, CONTAIN, DISJOINT, EQUAL, TOUCH, OVERLAP, WITHIN, CROSS = range(8)
NEW, ADD, WITHIN_CURRENT, REMOVE = range(4)
processing.run("native:selectbylocation", {
"INPUT": buildings,
"PREDICATE": [INTERSECT],
"INTERSECT": flood,
"METHOD": NEW,
})
print(buildings.selectedFeatureCount(), "buildings intersect flood zone 3")
Breakdown: PREDICATE is a list of integer codes, and naming them as constants at the top makes the call readable; the order above is the order the algorithm defines. Several predicates in the list are combined with OR, so [WITHIN, OVERLAP] selects features that are inside or partly inside. INTERSECT is the comparison layer, which confusingly shares its name with the first predicate. METHOD controls how the result combines with the existing selection. The relationship is always read as input predicate comparison — buildings are within the flood zone — which is why contain and are within are not interchangeable.
Combine with existing selections
Spatial and attribute selections compose. A common pattern is to narrow down by attribute first, then by location — or to subtract one spatial result from another.
from qgis.core import QgsProcessingFeatureSourceDefinition
buildings.selectByExpression("\"use\" = 'residential'")
processing.run("native:selectbylocation", {
"INPUT": buildings, "PREDICATE": [INTERSECT],
"INTERSECT": flood, "METHOD": WITHIN_CURRENT,
})
defences = QgsProject.instance().mapLayersByName("flood_defences")[0]
defences.selectByExpression("\"condition\" IN ('good', 'fair')")
protected = processing.run("native:buffer", {
"INPUT": QgsProcessingFeatureSourceDefinition(defences.id(), selectedFeaturesOnly=True),
"DISTANCE": 20, "DISSOLVE": True, "OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]
processing.run("native:selectbylocation", {
"INPUT": buildings, "PREDICATE": [INTERSECT],
"INTERSECT": protected, "METHOD": REMOVE,
})
print(buildings.selectedFeatureCount(), "residential buildings at risk and unprotected")
Breakdown: WITHIN_CURRENT keeps only already-selected features that also meet the predicate — the spatial equivalent of AND. REMOVE deselects matches — AND NOT. QgsProcessingFeatureSourceDefinition with selectedFeaturesOnly=True makes any algorithm use just the selected features of a layer, here only defences in good or fair condition. Dissolving the buffer produces one polygon rather than hundreds of overlapping ones, which makes the final location test faster. The chain reads like the question it answers, which is a real advantage when someone has to check the method later.
Two cautions apply to chained selections. First, every step changes state on a shared layer, so a script that is interrupted half-way leaves a partial selection behind that looks like a finished answer; clear the selection with removeSelection() at the start of the script, not only at the end. Second, a selection made in the Python console is visible to the user immediately and will be used by any tool they run next with "selected features only" ticked — convenient in an interactive session, surprising in a plugin that runs in the background. Plugins should usually extract to a temporary layer rather than borrow the user's selection.
Extract matching features to a new layer
When the result should be a layer rather than a selection — to hand to someone, to feed another algorithm, or simply because selections are lost when the project closes — use native:extractbylocation with the same parameters and an output.
at_risk = processing.run("native:extractbylocation", {
"INPUT": QgsProcessingFeatureSourceDefinition(buildings.id(), selectedFeaturesOnly=True),
"PREDICATE": [INTERSECT],
"INTERSECT": flood,
"OUTPUT": "/data/work/buildings_at_risk.gpkg",
})["OUTPUT"]
wells = QgsProject.instance().mapLayersByName("private_wells")[0]
protected_areas = QgsProject.instance().mapLayersByName("source_protection_zones")[0]
unprotected = processing.run("native:extractbylocation", {
"INPUT": wells, "PREDICATE": [DISJOINT],
"INTERSECT": protected_areas, "OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]
print(unprotected.featureCount(), "wells outside every protection zone")
Breakdown: Feeding the selection into extractbylocation via the source definition keeps the attribute and spatial steps from the previous section. DISJOINT answers "not within any" correctly because it tests against the comparison layer as a whole: a well is extracted only if it touches none of the zones. Running WITHIN and then inverting the selection gives the same answer on points but not on lines or polygons, where features that straddle a boundary are neither within nor disjoint — a subtle difference worth being deliberate about.
Select by location in Python
The algorithms are the right default. Dropping to the geometry API makes sense when the selection is one step in a loop, when the test needs a tolerance, or when you want per-feature detail such as how much of each building lies in the zone.
from qgis.core import (
QgsSpatialIndex, QgsFeatureRequest, QgsCoordinateTransform, QgsGeometry,
)
transform = QgsCoordinateTransform(flood.crs(), buildings.crs(), QgsProject.instance())
zones = []
for z in flood.getFeatures():
g = z.geometry()
g.transform(transform)
zones.append(g)
zone = QgsGeometry.unaryUnion(zones)
engine = QgsGeometry.createGeometryEngine(zone.constGet())
engine.prepareGeometry()
index = QgsSpatialIndex(buildings.getFeatures(QgsFeatureRequest().setNoAttributes()))
candidates = index.intersects(zone.boundingBox())
selected, share = [], {}
for f in buildings.getFeatures(QgsFeatureRequest().setFilterFids(candidates)):
g = f.geometry()
if engine.intersects(g.constGet()):
selected.append(f.id())
share[f.id()] = g.intersection(zone).area() / g.area() if g.area() else 0
buildings.selectByIds(selected)
mostly_in = [fid for fid, s in share.items() if s >= 0.5]
print(len(selected), "intersect;", len(mostly_in), "have at least half their footprint in the zone")
Breakdown: The zone is reprojected into the buildings' CRS and unioned into one geometry so each building is tested once rather than against every zone polygon. A prepared geometry engine makes repeated intersects tests against the same large polygon much faster. The spatial index narrows candidates to buildings whose bounding boxes overlap the zone's, and setFilterFids fetches only those. Computing the share of each footprint inside the zone answers a question neither algorithm can: a building clipped by a sliver of the zone is technically intersecting and practically not at risk. The pattern follows building and using a spatial index and testing whether geometries intersect.
QGIS version compatibility
native:selectbylocation and native:extractbylocation replaced the qgis: versions in QGIS 3.12, with the same predicate codes. The predicate order has not changed through 3.44 or on the QGIS 4 series, but constants such as those defined above make a script robust if it ever does. QgsProcessingFeatureSourceDefinition with selectedFeaturesOnly has been available since 3.0.
Troubleshooting
- Nothing is selected. Invalid geometries in the comparison layer, or the layers have no real overlap because one has a wrong CRS assigned.
- Too much is selected.
INTERSECTincludes features that only touch the boundary; useWITHINorOVERLAPas appropriate. - The selection disappears. Selections are not saved with data; extract to a layer if the result must persist.
- Very slow on large polygon layers. Dissolve or simplify the comparison layer first.
- Different counts from the Python version. Different handling of boundary contact; compare predicates, not code paths.
Conclusion
Use native:selectbylocation to shape a selection and native:extractbylocation to write one out, naming predicate and method codes as constants so the call reads as a sentence. Combine methods to build complex selections step by step, be deliberate about disjoint versus inverted within, and move to prepared geometries and a spatial index when you need tolerances or per-feature measurements.
Frequently Asked Questions
Can I select features within a distance of another layer?
Buffer the comparison layer first and select by intersection, or use native:selectwithindistance where your version provides it.
Does selection by location work across CRSs? Yes; the comparison layer is reprojected on the fly. Check that both CRSs are correctly assigned.
How do I count matches per polygon instead of selecting?
Use native:countpointsinpolygon for points, or a spatial join with summary for other geometries.
Can the same layer be input and comparison?
Yes, for questions like "which parcels touch another parcel", though every feature matches itself for most predicates. Exclude self-matches by comparing feature ids in the Python route, or by using native:joinattributesbylocation and filtering out rows where the joined id equals the feature's own.
How do I save the selection so it survives closing the project?
Selections are session state. Store the selected ids in a field — set a flag column to 1 for selected features inside an edit session — or extract the selection to its own layer. For recurring selections, a saved expression or a map theme with a filtered layer is often cleaner than persisting ids that change when data is reloaded.