Vector Data Manipulation in QGIS with PyQGIS
Vector data manipulation forms the operational core of modern geospatial workflows. Whether you are cleaning municipal boundaries, extracting features from survey datasets, or preparing spatial layers for downstream analysis, mastering programmatic control over vector geometries and attributes is essential. This guide sits inside the broader discipline of Spatial Data Processing & Automation with PyQGIS: where that overview frames the whole pipeline, this page zooms in on the vector stage — loading, validating, editing, joining, cleaning, and exporting features. PyQGIS provides a robust Python API that bridges the gap between interactive desktop GIS and reproducible, script-driven pipelines, and every pattern below is written to run unattended.
The workflow is deliberately modular. Each section maps to one stage of the diagram, and each stage is a self-contained function you can lift into a larger script, a Processing algorithm, or a batch loop. Read it top to bottom for the mental model, then follow the inline links into the focused how-to guides when you need a complete, copy-paste recipe.
Prerequisites
Before executing any vector manipulation routines, ensure your environment meets the following baseline:
- QGIS 3.34 LTR (or 3.28 LTR) installed with its bundled Python 3.9+. Every snippet here is pinned to the 3.x API; version-specific caveats are called out inline.
- Access to the QGIS Python Console (
Plugins > Python Console), the Processing Toolbox, or an external IDE configured against the QGIS Python environment. - A working understanding of feature classes, attribute tables, and topology rules.
- Source datasets in common vector formats — GeoPackage, Shapefile, GeoJSON, or DXF/DWG.
- A clear grasp of how Coordinate Reference Systems affect measurement accuracy and geometry validity, because most subtle bugs in vector work are really CRS bugs.
- A dedicated project directory with explicit read/write permissions for intermediate outputs and log files.
Loading and Validating Vector Layers
Every reliable pipeline starts by loading the target layer and proving it is usable before a single edit is attempted. Early validation prevents cascading errors during spatial joins or metric calculations, where a single self-intersecting ring can silently drop features from a result set.
from qgis.core import QgsVectorLayer
def load_and_validate(layer_path: str) -> QgsVectorLayer:
layer = QgsVectorLayer(layer_path, "input_vector", "ogr")
if not layer.isValid():
raise FileNotFoundError(f"Layer failed to load: {layer_path}")
invalid_count = 0
for feature in layer.getFeatures():
if not feature.geometry().isGeosValid():
invalid_count += 1
if invalid_count > 0:
print(f"Warning: {invalid_count} features contain invalid geometries.")
else:
print("All geometries validated successfully.")
return layer
The QgsVectorLayer constructor initializes the data source through the OGR provider ("ogr"), which covers file-based formats; swap it for "postgres" or "memory" when the source differs. isValid() confirms the provider opened the source at all — a wrong path, a locked file, or an unsupported driver all surface here. The per-feature isGeosValid() check then leans on the GEOS topology engine to catch self-intersections, duplicate nodes, and unclosed rings. Distinguishing the two matters: a layer can be perfectly valid to open yet full of features that are invalid to process.
For large datasets, do not iterate the whole layer just to count problems. A QgsFeatureRequest lets you subset by attribute expression, bounding box, or a hard limit, and returns only the fields you actually read:
from qgis.core import QgsFeatureRequest
request = QgsFeatureRequest().setFilterExpression('"status" = \'active\'')
request.setSubsetOfAttributes(["id", "status"], layer.fields())
for feature in layer.getFeatures(request):
... # only active features, only two attributes loaded into memory
Editing Attributes and Computing Derived Fields
Once a layer is validated, the most common task is enriching its attribute table — adding a field and populating it from geometry or from other columns. Calculating polygon areas is the canonical example, and it exposes the single biggest trap in vector work: units follow the CRS. Planar geom.area() on a geographic CRS such as EPSG:4326 returns square degrees, which are meaningless as real-world area. The pattern below uses QgsDistanceArea for a CRS-aware ellipsoidal measurement instead.
from qgis.core import QgsField, QgsDistanceArea, QgsProject, QgsVectorLayer, edit
from qgis.PyQt.QtCore import QVariant
def add_area_field(layer: QgsVectorLayer, field_name: str = "area_ha"):
# Add the field only if it does not already exist
if layer.fields().indexFromName(field_name) == -1:
layer.dataProvider().addAttributes([QgsField(field_name, QVariant.Double)])
layer.updateFields()
da = QgsDistanceArea()
da.setSourceCrs(layer.crs(), QgsProject.instance().transformContext())
da.setEllipsoid("WGS84")
field_idx = layer.fields().indexFromName(field_name)
with edit(layer):
for feature in layer.getFeatures():
geom = feature.geometry()
if geom.isGeosValid():
area_m2 = da.measureArea(geom)
area_ha = area_m2 / 10000.0
layer.changeAttributeValue(feature.id(), field_idx, area_ha)
QgsDistanceArea produces accurate ellipsoidal results regardless of the layer's stored projection, so you never have to physically reproject to get a trustworthy number. Wrapping the update loop in the edit() context manager batches every change into one transaction — this is dramatically faster than per-feature disk writes and guarantees the edit session is committed (or rolled back on exception) cleanly. For a standalone, production-ready version of this exact task, follow the PyQGIS script to calculate polygon areas, which also covers the planar-vs-geodesic decision and a native:fieldcalculator alternative.
Spatial Filtering, Selection, and Joins
Attribute edits change what a feature knows about itself; spatial operations change what it knows about its neighbours. Filtering by a spatial predicate — intersects, contains, within — is the foundation of clipping, tagging, and joining. Doing it feature-by-feature with a nested loop is O(n×m) and collapses on real data, so build a QgsSpatialIndex over the reference layer first and query only the candidates whose bounding boxes overlap.
from qgis.core import QgsSpatialIndex, QgsVectorLayer
def tag_points_by_zone(points: QgsVectorLayer, zones: QgsVectorLayer,
zone_attr: str = "zone_name"):
index = QgsSpatialIndex(zones.getFeatures())
zone_lookup = {f.id(): f for f in zones.getFeatures()}
zone_idx = points.fields().indexFromName("zone")
from qgis.core import QgsField
from qgis.PyQt.QtCore import QVariant
if zone_idx == -1:
points.dataProvider().addAttributes([QgsField("zone", QVariant.String)])
points.updateFields()
zone_idx = points.fields().indexFromName("zone")
with edit(points):
for pt in points.getFeatures():
geom = pt.geometry()
# Narrow to bounding-box candidates, then test the exact predicate
for cand_id in index.intersects(geom.boundingBox()):
zone = zone_lookup[cand_id]
if zone.geometry().contains(geom):
points.changeAttributeValue(
pt.id(), zone_idx, zone[zone_attr])
break
The index turns a full scan into a two-step filter: index.intersects() returns cheap bounding-box hits, and only those candidates get the exact, expensive contains() test. A critical precondition is that both layers share the same CRS — comparing geometries across projections silently returns wrong answers rather than raising an error. Reproject the reference layer with QgsCoordinateTransform first when they differ, and see Coordinate Reference Systems for the transform mechanics. When the operation is a hard spatial cut rather than a tag, the dedicated clip a vector layer in PyQGIS recipe wraps the native:clip algorithm and handles the edge cases.
Cleaning and Repairing Geometry
Real-world datasets frequently contain sliver polygons, duplicate nodes, or unclosed rings. Left unrepaired, these break overlays and produce phantom features. The routine below filters out sub-threshold slivers, repairs invalid geometry with the modern GEOS makeValid() method, and stages the survivors in a temporary memory layer before writing them out.
from qgis.core import (
QgsVectorLayer, QgsFeature, QgsVectorFileWriter, QgsProject,
)
def filter_and_clean(layer: QgsVectorLayer, min_area_m2: float = 100.0) -> QgsVectorLayer:
# Create a memory layer matching the source schema
mem_layer = QgsVectorLayer(
"Polygon?crs={}".format(layer.crs().authid()), "cleaned_temp", "memory"
)
mem_layer.dataProvider().addAttributes(layer.fields())
mem_layer.updateFields()
mem_layer.startEditing()
for feature in layer.getFeatures():
geom = feature.geometry()
# Drop slivers below the area threshold
if geom.area() < min_area_m2:
continue
# Repair topology using the modern GEOS method
if not geom.isGeosValid():
geom = geom.makeValid()
new_feat = QgsFeature()
new_feat.setGeometry(geom)
new_feat.setAttributes(feature.attributes())
mem_layer.addFeature(new_feat)
mem_layer.commitChanges()
return mem_layer
makeValid() is the GEOS-backed standard for resolving self-intersecting polygons while preserving their structure and attributes. Prefer it over the legacy zero-buffer trick (geom.buffer(0)), which collapses slivers, merges rings unpredictably, and can distort otherwise valid geometry. For batch jobs across many files, the equivalent Processing algorithm is native:fixgeometries, which you can drop straight into a pipeline — see Chaining Processing Algorithms in PyQGIS for how to wire it between other steps, and Batch Processing with PyQGIS for running it over a folder of layers.
Exporting and Converting Formats
The final stage serializes the processed layer into a standardized format for downstream consumption. writeAsVectorFormatV3 is the current writer: it exposes the SaveVectorOptions API for driver selection, encoding, and CRS transforms, and returns a clear error tuple you can assert on.
from qgis.core import QgsVectorFileWriter, QgsProject
def export_layer(layer, output_path: str, driver: str = "GPKG"):
options = QgsVectorFileWriter.SaveVectorOptions()
options.driverName = driver
error_code, error_msg, _, _ = QgsVectorFileWriter.writeAsVectorFormatV3(
layer, output_path, QgsProject.instance().transformContext(), options
)
if error_code != QgsVectorFileWriter.NoError:
raise RuntimeError(f"Export failed: {error_msg}")
return output_path
Choose the driver to fit the consumer: "GPKG" for a robust, multi-layer working format, or "GeoJSON" for web delivery. When the target is the browser, standardizing on GeoJSON while preserving the attribute schema is a task in its own right — the automating shapefile to GeoJSON conversion guide covers precision control and encoding pitfalls. When you instead need to fold many inputs into one output, merge multiple shapefiles in PyQGIS shows how to reconcile schemas before the write. Cleaned vector layers also feed the next stage of analysis: they frequently serve as masks in Raster Analysis Workflows in PyQGIS.
Common Errors and Troubleshooting
Programmatic vector manipulation introduces several failure points that are easily mitigated with defensive coding.
QgsVectorLayer fails to initialize. Cause: incorrect provider string, missing file permissions, or an unsupported format. Fix: confirm the OGR driver supports the input, and use the three-argument constructor QgsVectorLayer(path, "name", "ogr"). Legacy CAD files (DXF/DWG) often need preprocessing before PyQGIS can parse their geometry.
CRS mismatch during spatial operations. Cause: running distance/area calculations or joins on layers with differing coordinate systems. Fix: verify layer.crs().isValid() and that both operands share a CRS; align them with QgsCoordinateTransform, or rely on QgsDistanceArea for measurements, which handles ellipsoidal math without physical reprojection.
MemoryError on large datasets. Cause: loading whole layers into memory or iterating without chunking. Fix: use QgsFeatureRequest with setLimit(), setFilterExpression(), or setSubsetOfAttributes() to subset data, write to a file-based GeoPackage instead of a memory layer, and process in spatial tiles for enterprise-scale jobs.
Attribute update appears to succeed but never persists. Cause: edits made outside an edit session, or a read-only source. Fix: wrap changes in with edit(layer):, confirm the provider is writable (GeoPackage and PostGIS are; some are not), and ensure Shapefile sidecars (.dbf, .shx) have write permission.
Choosing your level of abstraction
The same task can be written three ways in PyQGIS, and picking the wrong level is the usual reason a script is either slower than it should be or more complicated than it needs to be.
The mistake worth naming is reaching for the bottom level too early. A Python loop that buffers and clips each feature individually is doing in interpreted code what native:buffer does in C++, and on a layer of any size it will be slower by a wide margin while also being longer and harder to get right. Drop down a level only when the operation genuinely needs a decision the algorithm cannot express.
The reverse mistake is chaining six algorithms where a single loop would do, each writing an intermediate GeoPackage to disk. When several operations apply to the same features and no intermediate is wanted, doing the geometry work directly avoids all of that I/O.
Key Takeaways
- Validate before you manipulate. Separate
isValid()(can the source open?) from per-featureisGeosValid()(is this geometry safe to process?) and repair problems at ingestion, not at export. - Units follow the CRS. Use
QgsDistanceAreafor area and length so results stay correct regardless of the layer's projection, and keep both operands in the same CRS for every spatial predicate. - Index your spatial joins. A
QgsSpatialIndexbounding-box pre-filter turns an unusable O(n×m) scan into a workable two-step query. - Prefer modern APIs.
makeValid()overbuffer(0),writeAsVectorFormatV3over deprecated writers, and theedit()context manager over manualstartEditing()/commitChanges(). - Keep each stage modular. Load, edit, join, clean, and export as separate functions so they drop straight into chained pipelines and batch loops.
Frequently Asked Questions
Why should I validate geometries with isGeosValid() before manipulating a vector layer?
Invalid geometries — self-intersections, duplicate nodes, or unclosed rings — cause spatial predicates and overlay operations to return unpredictable results or drop features silently. Checking isGeosValid() during ingestion lets you flag and repair these features before they corrupt downstream joins or area calculations. Catching problems early is far cheaper than debugging a failed pipeline at the export stage.
What is the difference between makeValid() and the old zero-buffer trick for fixing polygons?makeValid() is the modern GEOS-backed method that repairs self-intersecting or malformed polygons while preserving their structure and attributes. The legacy zero-buffer workaround (geom.buffer(0)) often collapses slivers, merges rings unpredictably, and can distort valid geometry. On QGIS 3.34 LTR you should always prefer makeValid(), or the native:fixgeometries algorithm for batch jobs.
Should I use writeAsVectorFormatV3 or the older writer methods for exporting?
Use writeAsVectorFormatV3 on any modern QGIS install (3.16+, including 3.34 LTR). It exposes the SaveVectorOptions API for driver selection, encoding, and CRS transforms, and returns a clear error tuple you can check. The V2 method works on older releases, but the original writeAsVectorFormat is deprecated and lacks transactional safety.
How do I make spatial joins fast on large layers?
Build a QgsSpatialIndex over the reference layer, then use index.intersects(geom.boundingBox()) to shortlist candidates before running the exact predicate (contains, intersects, within). This avoids testing every feature against every other feature. Make sure both layers share the same CRS first, otherwise the geometry comparisons are meaningless.
How do I avoid MemoryError when manipulating very large vector layers?
Do not load entire layers into memory at once. Use QgsFeatureRequest with setFilterExpression(), setLimit(), or setSubsetOfAttributes() to subset features, process data in spatial tiles, and write outputs to file-based formats like GeoPackage instead of memory layers. For repeated runs, GeoPackage transactional edits keep memory usage flat regardless of dataset size.
Why does my attribute update appear to succeed but the values never persist?
This almost always means the edits were never committed. Wrap changes in the with edit(layer): context manager, which opens and commits the edit session automatically, or call layer.commitChanges() explicitly. Also confirm the data source is writable — Shapefiles need write permission on the .dbf/.shx, and some providers are read-only.
Should I use a Processing algorithm or a feature loop? The algorithm whenever one exists for the operation. It runs in compiled code, handles the output writing and reports progress, and it is much harder to get wrong than an equivalent loop. Drop to a loop only when the logic needs a per-feature decision the algorithm cannot express.
Why is my feature loop so slow?
Usually because it reads more than it needs. Add QgsFeatureRequest.NoGeometry when the geometry is not used, restrict the columns with setSubsetOfAttributes(), and push any filtering into setFilterExpression() so the provider does it rather than Python.
What is the safest output format? GeoPackage. It has no field-name length limit, full Unicode support, real date and boolean types, and it stores everything in a single file — all of which are places where shapefile silently changes your data.
How do I check a layer loaded correctly?layer.isValid(). An invalid layer is not None — it is a real object that returns zero features and an empty extent, so code that only checks for None proceeds happily on nothing at all.
Does deleting features renumber the remaining feature ids? No — identifiers are provider-assigned and stable for a given feature.
Related
- Spatial Data Processing & Automation with PyQGIS — the parent overview this guide belongs to
- Coordinate Reference Systems in PyQGIS
- Raster Analysis Workflows in PyQGIS
- Chaining Processing Algorithms in PyQGIS
- Batch Processing with PyQGIS
- PyQGIS Script to Calculate Polygon Areas
- Automating Shapefile to GeoJSON Conversion in QGIS
- Clip a Vector Layer in PyQGIS
- Merge Multiple Shapefiles in PyQGIS