[{"data":1,"prerenderedAt":2246},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation":3},{"id":4,"title":5,"body":6,"description":2234,"extension":2235,"meta":2236,"navigation":264,"path":2242,"seo":2243,"stem":2244,"__hash__":2245},"docs\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Findex.md","Vector Data Manipulation in PyQGIS",{"type":7,"value":8,"toc":2221},"minimark",[9,14,24,27,176,181,184,220,224,232,495,526,533,616,620,630,839,857,861,876,986,1234,1261,1265,1271,1497,1520,1524,1535,1667,1692,1696,1699,1719,1740,1768,1790,1794,1797,1912,1919,1922,1926,1987,1991,2003,2023,2042,2065,2083,2101,2107,2123,2129,2145,2151,2155,2217],[10,11,13],"h1",{"id":12},"vector-data-manipulation-in-qgis-with-pyqgis","Vector Data Manipulation in QGIS with PyQGIS",[15,16,17,18,23],"p",{},"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 ",[19,20,22],"a",{"href":21},"\u002Fspatial-data-processing-automation\u002F","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.",[15,25,26],{},"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.",[28,29,34,38,42,50,60,69,75,79,83,87,91,95,99,103,107,111,114,117,120,125,130,134,137,140,147,151,155,160],"svg",{"viewBox":30,"role":31,"ariaLabel":32,"xmlns":33},"4 54 752 194","img","A four-stage PyQGIS vector data workflow from loading through export","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[35,36,37],"title",{},"PyQGIS vector data manipulation workflow",[39,40,41],"desc",{},"Load and validate a layer, run attribute and spatial operations, clean geometry with makeValid, then export with writeAsVectorFormatV3.",[43,44],"rect",{"x":45,"y":46,"width":47,"height":48,"fill":49},"4","54","752","194","#f6f3ea",[43,51],{"x":52,"y":53,"width":54,"height":55,"rx":56,"fill":57,"stroke":58,"style":59},"20","70","165","110","10","#fffdf7","#0f766e","stroke-width:2.5",[61,62,68],"text",{"x":63,"y":64,"style":65,"fill":66,"textAnchor":67},"102","100","text-anchor:middle;font-size:13px;font-weight:bold;font-family:sans-serif","#17211d","middle","1. Load & validate",[61,70,74],{"x":63,"y":71,"style":72,"fill":73,"textAnchor":67},"126","text-anchor:middle;font-size:12px;font-family:sans-serif","#2f3b35","QgsVectorLayer",[61,76,78],{"x":63,"y":77,"style":72,"fill":73,"textAnchor":67},"148","isValid()",[61,80,82],{"x":63,"y":81,"style":72,"fill":73,"textAnchor":67},"168","isGeosValid()",[43,84],{"x":85,"y":53,"width":54,"height":55,"rx":56,"fill":57,"stroke":86,"style":59},"205","#2563eb",[61,88,90],{"x":89,"y":64,"style":65,"fill":66,"textAnchor":67},"287","2. Attribute &",[61,92,94],{"x":89,"y":93,"style":65,"fill":66,"textAnchor":67},"118","spatial ops",[61,96,98],{"x":89,"y":97,"style":72,"fill":73,"textAnchor":67},"144","Field calc, filter,",[61,100,102],{"x":89,"y":101,"style":72,"fill":73,"textAnchor":67},"164","QgsDistanceArea",[43,104],{"x":105,"y":53,"width":54,"height":55,"rx":56,"fill":57,"stroke":106,"style":59},"390","#b45309",[61,108,110],{"x":109,"y":64,"style":65,"fill":66,"textAnchor":67},"472","3. Geometry",[61,112,113],{"x":109,"y":93,"style":65,"fill":66,"textAnchor":67},"cleaning",[61,115,116],{"x":109,"y":97,"style":72,"fill":73,"textAnchor":67},"makeValid()",[61,118,119],{"x":109,"y":101,"style":72,"fill":73,"textAnchor":67},"drop slivers",[43,121],{"x":122,"y":53,"width":54,"height":55,"rx":56,"fill":123,"stroke":124,"style":59},"575","#26322d","#15803d",[61,126,129],{"x":127,"y":64,"style":65,"fill":128,"textAnchor":67},"657","#d9f99d","4. Export",[61,131,133],{"x":127,"y":132,"style":72,"fill":128,"textAnchor":67},"130","writeAsVector",[61,135,136],{"x":127,"y":77,"style":72,"fill":128,"textAnchor":67},"FormatV3()",[61,138,139],{"x":127,"y":81,"style":72,"fill":128,"textAnchor":67},"GPKG \u002F GeoJSON",[141,142],"line",{"x1":143,"y1":144,"x2":145,"y2":144,"stroke":73,"style":146},"185","125","203","stroke-width:2.5;marker-end:url(#vw)",[141,148],{"x1":149,"y1":144,"x2":150,"y2":144,"stroke":73,"style":146},"370","388",[141,152],{"x1":153,"y1":144,"x2":154,"y2":144,"stroke":73,"style":146},"555","573",[61,156,159],{"x":157,"y":158,"style":72,"fill":73,"textAnchor":67},"380","230","Each stage is modular and chainable into batch automation scripts.",[161,162,163],"defs",{},[164,165,172],"marker",{"id":166,"markerWidth":167,"markerHeight":167,"refX":168,"refY":169,"orient":170,"markerUnits":171},"vw","9","7","3","auto","strokeWidth",[173,174],"path",{"d":175,"fill":73},"M0,0 L7,3 L0,6 Z",[177,178,180],"h2",{"id":179},"prerequisites","Prerequisites",[15,182,183],{},"Before executing any vector manipulation routines, ensure your environment meets the following baseline:",[185,186,187,195,203,206,209,217],"ul",{},[188,189,190,194],"li",{},[191,192,193],"strong",{},"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.",[188,196,197,198,202],{},"Access to the QGIS Python Console (",[199,200,201],"code",{},"Plugins > Python Console","), the Processing Toolbox, or an external IDE configured against the QGIS Python environment.",[188,204,205],{},"A working understanding of feature classes, attribute tables, and topology rules.",[188,207,208],{},"Source datasets in common vector formats — GeoPackage, Shapefile, GeoJSON, or DXF\u002FDWG.",[188,210,211,212,216],{},"A clear grasp of how ",[19,213,215],{"href":214},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems"," affect measurement accuracy and geometry validity, because most subtle bugs in vector work are really CRS bugs.",[188,218,219],{},"A dedicated project directory with explicit read\u002Fwrite permissions for intermediate outputs and log files.",[177,221,223],{"id":222},"loading-and-validating-vector-layers","Loading and Validating Vector Layers",[15,225,226,227,231],{},"Every reliable pipeline starts by loading the target layer and proving it is usable ",[228,229,230],"em",{},"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.",[233,234,239],"pre",{"className":235,"code":236,"language":237,"meta":238,"style":238},"language-python shiki shiki-themes github-dark","from qgis.core import QgsVectorLayer\n\n\ndef load_and_validate(layer_path: str) -> QgsVectorLayer:\n    layer = QgsVectorLayer(layer_path, \"input_vector\", \"ogr\")\n    if not layer.isValid():\n        raise FileNotFoundError(f\"Layer failed to load: {layer_path}\")\n\n    invalid_count = 0\n    for feature in layer.getFeatures():\n        if not feature.geometry().isGeosValid():\n            invalid_count += 1\n\n    if invalid_count > 0:\n        print(f\"Warning: {invalid_count} features contain invalid geometries.\")\n    else:\n        print(\"All geometries validated successfully.\")\n\n    return layer\n","python","",[199,240,241,259,266,271,291,316,328,360,365,376,391,402,414,419,436,461,469,481,486],{"__ignoreMap":238},[242,243,245,249,253,256],"span",{"class":141,"line":244},1,[242,246,248],{"class":247},"snl16","from",[242,250,252],{"class":251},"s95oV"," qgis.core ",[242,254,255],{"class":247},"import",[242,257,258],{"class":251}," QgsVectorLayer\n",[242,260,262],{"class":141,"line":261},2,[242,263,265],{"emptyLinePlaceholder":264},true,"\n",[242,267,269],{"class":141,"line":268},3,[242,270,265],{"emptyLinePlaceholder":264},[242,272,274,277,281,284,288],{"class":141,"line":273},4,[242,275,276],{"class":247},"def",[242,278,280],{"class":279},"svObZ"," load_and_validate",[242,282,283],{"class":251},"(layer_path: ",[242,285,287],{"class":286},"sDLfK","str",[242,289,290],{"class":251},") -> QgsVectorLayer:\n",[242,292,294,297,300,303,307,310,313],{"class":141,"line":293},5,[242,295,296],{"class":251},"    layer ",[242,298,299],{"class":247},"=",[242,301,302],{"class":251}," QgsVectorLayer(layer_path, ",[242,304,306],{"class":305},"sU2Wk","\"input_vector\"",[242,308,309],{"class":251},", ",[242,311,312],{"class":305},"\"ogr\"",[242,314,315],{"class":251},")\n",[242,317,319,322,325],{"class":141,"line":318},6,[242,320,321],{"class":247},"    if",[242,323,324],{"class":247}," not",[242,326,327],{"class":251}," layer.isValid():\n",[242,329,331,334,337,340,343,346,349,352,355,358],{"class":141,"line":330},7,[242,332,333],{"class":247},"        raise",[242,335,336],{"class":286}," FileNotFoundError",[242,338,339],{"class":251},"(",[242,341,342],{"class":247},"f",[242,344,345],{"class":305},"\"Layer failed to load: ",[242,347,348],{"class":286},"{",[242,350,351],{"class":251},"layer_path",[242,353,354],{"class":286},"}",[242,356,357],{"class":305},"\"",[242,359,315],{"class":251},[242,361,363],{"class":141,"line":362},8,[242,364,265],{"emptyLinePlaceholder":264},[242,366,368,371,373],{"class":141,"line":367},9,[242,369,370],{"class":251},"    invalid_count ",[242,372,299],{"class":247},[242,374,375],{"class":286}," 0\n",[242,377,379,382,385,388],{"class":141,"line":378},10,[242,380,381],{"class":247},"    for",[242,383,384],{"class":251}," feature ",[242,386,387],{"class":247},"in",[242,389,390],{"class":251}," layer.getFeatures():\n",[242,392,394,397,399],{"class":141,"line":393},11,[242,395,396],{"class":247},"        if",[242,398,324],{"class":247},[242,400,401],{"class":251}," feature.geometry().isGeosValid():\n",[242,403,405,408,411],{"class":141,"line":404},12,[242,406,407],{"class":251},"            invalid_count ",[242,409,410],{"class":247},"+=",[242,412,413],{"class":286}," 1\n",[242,415,417],{"class":141,"line":416},13,[242,418,265],{"emptyLinePlaceholder":264},[242,420,422,424,427,430,433],{"class":141,"line":421},14,[242,423,321],{"class":247},[242,425,426],{"class":251}," invalid_count ",[242,428,429],{"class":247},">",[242,431,432],{"class":286}," 0",[242,434,435],{"class":251},":\n",[242,437,439,442,444,446,449,451,454,456,459],{"class":141,"line":438},15,[242,440,441],{"class":286},"        print",[242,443,339],{"class":251},[242,445,342],{"class":247},[242,447,448],{"class":305},"\"Warning: ",[242,450,348],{"class":286},[242,452,453],{"class":251},"invalid_count",[242,455,354],{"class":286},[242,457,458],{"class":305}," features contain invalid geometries.\"",[242,460,315],{"class":251},[242,462,464,467],{"class":141,"line":463},16,[242,465,466],{"class":247},"    else",[242,468,435],{"class":251},[242,470,472,474,476,479],{"class":141,"line":471},17,[242,473,441],{"class":286},[242,475,339],{"class":251},[242,477,478],{"class":305},"\"All geometries validated successfully.\"",[242,480,315],{"class":251},[242,482,484],{"class":141,"line":483},18,[242,485,265],{"emptyLinePlaceholder":264},[242,487,489,492],{"class":141,"line":488},19,[242,490,491],{"class":247},"    return",[242,493,494],{"class":251}," layer\n",[15,496,497,498,500,501,503,504,507,508,511,512,514,515,517,518,521,522,525],{},"The ",[199,499,74],{}," constructor initializes the data source through the OGR provider (",[199,502,312],{},"), which covers file-based formats; swap it for ",[199,505,506],{},"\"postgres\""," or ",[199,509,510],{},"\"memory\""," when the source differs. ",[199,513,78],{}," confirms the provider opened the source at all — a wrong path, a locked file, or an unsupported driver all surface here. The per-feature ",[199,516,82],{}," 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 ",[228,519,520],{},"open"," yet full of features that are invalid to ",[228,523,524],{},"process",".",[15,527,528,529,532],{},"For large datasets, do not iterate the whole layer just to count problems. A ",[199,530,531],{},"QgsFeatureRequest"," lets you subset by attribute expression, bounding box, or a hard limit, and returns only the fields you actually read:",[233,534,536],{"className":235,"code":535,"language":237,"meta":238,"style":238},"from qgis.core import QgsFeatureRequest\n\nrequest = QgsFeatureRequest().setFilterExpression('\"status\" = \\'active\\'')\nrequest.setSubsetOfAttributes([\"id\", \"status\"], layer.fields())\nfor feature in layer.getFeatures(request):\n    ...  # only active features, only two attributes loaded into memory\n",[199,537,538,549,553,579,595,607],{"__ignoreMap":238},[242,539,540,542,544,546],{"class":141,"line":244},[242,541,248],{"class":247},[242,543,252],{"class":251},[242,545,255],{"class":247},[242,547,548],{"class":251}," QgsFeatureRequest\n",[242,550,551],{"class":141,"line":261},[242,552,265],{"emptyLinePlaceholder":264},[242,554,555,558,560,563,566,569,572,574,577],{"class":141,"line":268},[242,556,557],{"class":251},"request ",[242,559,299],{"class":247},[242,561,562],{"class":251}," QgsFeatureRequest().setFilterExpression(",[242,564,565],{"class":305},"'\"status\" = ",[242,567,568],{"class":286},"\\'",[242,570,571],{"class":305},"active",[242,573,568],{"class":286},[242,575,576],{"class":305},"'",[242,578,315],{"class":251},[242,580,581,584,587,589,592],{"class":141,"line":273},[242,582,583],{"class":251},"request.setSubsetOfAttributes([",[242,585,586],{"class":305},"\"id\"",[242,588,309],{"class":251},[242,590,591],{"class":305},"\"status\"",[242,593,594],{"class":251},"], layer.fields())\n",[242,596,597,600,602,604],{"class":141,"line":293},[242,598,599],{"class":247},"for",[242,601,384],{"class":251},[242,603,387],{"class":247},[242,605,606],{"class":251}," layer.getFeatures(request):\n",[242,608,609,612],{"class":141,"line":318},[242,610,611],{"class":286},"    ...",[242,613,615],{"class":614},"sjoCn","  # only active features, only two attributes loaded into memory\n",[177,617,619],{"id":618},"editing-attributes-and-computing-derived-fields","Editing Attributes and Computing Derived Fields",[15,621,622,623,626,627,629],{},"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 ",[199,624,625],{},"geom.area()"," on a geographic CRS such as EPSG:4326 returns square degrees, which are meaningless as real-world area. The pattern below uses ",[199,628,102],{}," for a CRS-aware ellipsoidal measurement instead.",[233,631,633],{"className":235,"code":632,"language":237,"meta":238,"style":238},"from qgis.core import QgsField, QgsDistanceArea, QgsProject, QgsVectorLayer, edit\nfrom qgis.PyQt.QtCore import QVariant\n\n\ndef add_area_field(layer: QgsVectorLayer, field_name: str = \"area_ha\"):\n    # Add the field only if it does not already exist\n    if layer.fields().indexFromName(field_name) == -1:\n        layer.dataProvider().addAttributes([QgsField(field_name, QVariant.Double)])\n        layer.updateFields()\n\n    da = QgsDistanceArea()\n    da.setSourceCrs(layer.crs(), QgsProject.instance().transformContext())\n    da.setEllipsoid(\"WGS84\")\n\n    field_idx = layer.fields().indexFromName(field_name)\n\n    with edit(layer):\n        for feature in layer.getFeatures():\n            geom = feature.geometry()\n            if geom.isGeosValid():\n                area_m2 = da.measureArea(geom)\n                area_ha = area_m2 \u002F 10000.0\n                layer.changeAttributeValue(feature.id(), field_idx, area_ha)\n",[199,634,635,646,658,662,666,687,692,710,715,720,724,734,739,749,753,763,767,775,786,796,805,816,833],{"__ignoreMap":238},[242,636,637,639,641,643],{"class":141,"line":244},[242,638,248],{"class":247},[242,640,252],{"class":251},[242,642,255],{"class":247},[242,644,645],{"class":251}," QgsField, QgsDistanceArea, QgsProject, QgsVectorLayer, edit\n",[242,647,648,650,653,655],{"class":141,"line":261},[242,649,248],{"class":247},[242,651,652],{"class":251}," qgis.PyQt.QtCore ",[242,654,255],{"class":247},[242,656,657],{"class":251}," QVariant\n",[242,659,660],{"class":141,"line":268},[242,661,265],{"emptyLinePlaceholder":264},[242,663,664],{"class":141,"line":273},[242,665,265],{"emptyLinePlaceholder":264},[242,667,668,670,673,676,678,681,684],{"class":141,"line":293},[242,669,276],{"class":247},[242,671,672],{"class":279}," add_area_field",[242,674,675],{"class":251},"(layer: QgsVectorLayer, field_name: ",[242,677,287],{"class":286},[242,679,680],{"class":247}," =",[242,682,683],{"class":305}," \"area_ha\"",[242,685,686],{"class":251},"):\n",[242,688,689],{"class":141,"line":318},[242,690,691],{"class":614},"    # Add the field only if it does not already exist\n",[242,693,694,696,699,702,705,708],{"class":141,"line":330},[242,695,321],{"class":247},[242,697,698],{"class":251}," layer.fields().indexFromName(field_name) ",[242,700,701],{"class":247},"==",[242,703,704],{"class":247}," -",[242,706,707],{"class":286},"1",[242,709,435],{"class":251},[242,711,712],{"class":141,"line":362},[242,713,714],{"class":251},"        layer.dataProvider().addAttributes([QgsField(field_name, QVariant.Double)])\n",[242,716,717],{"class":141,"line":367},[242,718,719],{"class":251},"        layer.updateFields()\n",[242,721,722],{"class":141,"line":378},[242,723,265],{"emptyLinePlaceholder":264},[242,725,726,729,731],{"class":141,"line":393},[242,727,728],{"class":251},"    da ",[242,730,299],{"class":247},[242,732,733],{"class":251}," QgsDistanceArea()\n",[242,735,736],{"class":141,"line":404},[242,737,738],{"class":251},"    da.setSourceCrs(layer.crs(), QgsProject.instance().transformContext())\n",[242,740,741,744,747],{"class":141,"line":416},[242,742,743],{"class":251},"    da.setEllipsoid(",[242,745,746],{"class":305},"\"WGS84\"",[242,748,315],{"class":251},[242,750,751],{"class":141,"line":421},[242,752,265],{"emptyLinePlaceholder":264},[242,754,755,758,760],{"class":141,"line":438},[242,756,757],{"class":251},"    field_idx ",[242,759,299],{"class":247},[242,761,762],{"class":251}," layer.fields().indexFromName(field_name)\n",[242,764,765],{"class":141,"line":463},[242,766,265],{"emptyLinePlaceholder":264},[242,768,769,772],{"class":141,"line":471},[242,770,771],{"class":247},"    with",[242,773,774],{"class":251}," edit(layer):\n",[242,776,777,780,782,784],{"class":141,"line":483},[242,778,779],{"class":247},"        for",[242,781,384],{"class":251},[242,783,387],{"class":247},[242,785,390],{"class":251},[242,787,788,791,793],{"class":141,"line":488},[242,789,790],{"class":251},"            geom ",[242,792,299],{"class":247},[242,794,795],{"class":251}," feature.geometry()\n",[242,797,799,802],{"class":141,"line":798},20,[242,800,801],{"class":247},"            if",[242,803,804],{"class":251}," geom.isGeosValid():\n",[242,806,808,811,813],{"class":141,"line":807},21,[242,809,810],{"class":251},"                area_m2 ",[242,812,299],{"class":247},[242,814,815],{"class":251}," da.measureArea(geom)\n",[242,817,819,822,824,827,830],{"class":141,"line":818},22,[242,820,821],{"class":251},"                area_ha ",[242,823,299],{"class":247},[242,825,826],{"class":251}," area_m2 ",[242,828,829],{"class":247},"\u002F",[242,831,832],{"class":286}," 10000.0\n",[242,834,836],{"class":141,"line":835},23,[242,837,838],{"class":251},"                layer.changeAttributeValue(feature.id(), field_idx, area_ha)\n",[15,840,841,843,844,847,848,852,853,856],{},[199,842,102],{}," 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 ",[199,845,846],{},"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 ",[19,849,851],{"href":850},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fpyqgis-script-to-calculate-polygon-areas\u002F","PyQGIS script to calculate polygon areas",", which also covers the planar-vs-geodesic decision and a ",[199,854,855],{},"native:fieldcalculator"," alternative.",[177,858,860],{"id":859},"spatial-filtering-selection-and-joins","Spatial Filtering, Selection, and Joins",[15,862,863,864,867,868,871,872,875],{},"Attribute edits change ",[228,865,866],{},"what a feature knows about itself","; spatial operations change ",[228,869,870],{},"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 ",[199,873,874],{},"QgsSpatialIndex"," over the reference layer first and query only the candidates whose bounding boxes overlap.",[28,877,880,883,886,891,895,900,905,908,912,915,919,922,925,928,932,935,939,942,946,949,952,964,970,974,978,982],{"viewBox":878,"role":31,"ariaLabel":879,"xmlns":33},"0 0 760 250","How a QgsSpatialIndex turns a full O(n by m) spatial scan into a two-step bounding-box then exact-predicate query",[35,881,882],{},"Indexed spatial join: bounding-box pre-filter then exact predicate",[39,884,885],{},"A query feature's bounding box is passed to a QgsSpatialIndex, which returns a small candidate shortlist by cheap box tests; only those candidates are run through the exact predicate such as contains, turning an O(n by m) full scan into a workable two-step query.",[43,887],{"x":888,"y":888,"width":889,"height":890,"fill":49},"0","760","250",[61,892,894],{"x":157,"y":893,"style":65,"fill":66,"textAnchor":67},"30","Spatial join: index first, then test the exact predicate",[43,896],{"x":897,"y":46,"width":898,"height":899,"rx":56,"fill":57,"stroke":86,"style":59},"14","160","104",[61,901,904],{"x":902,"y":903,"style":65,"fill":66,"textAnchor":67},"94","84","1. Query feature",[61,906,907],{"x":902,"y":55,"style":72,"fill":73,"textAnchor":67},"geom.boundingBox()",[61,909,911],{"x":902,"y":910,"style":72,"fill":73,"textAnchor":67},"132","one cheap rectangle",[43,913],{"x":914,"y":46,"width":898,"height":899,"rx":56,"fill":123,"stroke":58,"style":59},"200",[61,916,918],{"x":917,"y":903,"style":65,"fill":128,"textAnchor":67},"280","2. QgsSpatialIndex",[61,920,921],{"x":917,"y":55,"style":72,"fill":128,"textAnchor":67},"index.intersects(bbox)",[61,923,924],{"x":917,"y":910,"style":72,"fill":128,"textAnchor":67},"fast box test",[43,926],{"x":927,"y":46,"width":898,"height":899,"rx":56,"fill":57,"stroke":106,"style":59},"386",[61,929,931],{"x":930,"y":903,"style":65,"fill":66,"textAnchor":67},"466","3. Candidate",[61,933,934],{"x":930,"y":63,"style":65,"fill":66,"textAnchor":67},"shortlist",[61,936,938],{"x":930,"y":937,"style":72,"fill":73,"textAnchor":67},"128","a few zones, not all",[43,940],{"x":941,"y":46,"width":898,"height":899,"rx":56,"fill":57,"stroke":124,"style":59},"572",[61,943,945],{"x":944,"y":903,"style":65,"fill":66,"textAnchor":67},"652","4. Exact predicate",[61,947,948],{"x":944,"y":55,"style":72,"fill":73,"textAnchor":67},"contains() on the",[61,950,951],{"x":944,"y":937,"style":72,"fill":73,"textAnchor":67},"candidates only",[161,953,954],{},[164,955,961],{"id":956,"viewBox":957,"refX":958,"refY":959,"markerWidth":168,"markerHeight":168,"orient":960},"sixarrow","0 0 10 10","8","5","auto-start-reverse",[173,962],{"d":963,"fill":73},"M0 0 L10 5 L0 10 z",[141,965],{"x1":966,"y1":967,"x2":968,"y2":967,"stroke":73,"style":969},"174","106","198","stroke-width:2.5;marker-end:url(#sixarrow)",[141,971],{"x1":972,"y1":967,"x2":973,"y2":967,"stroke":73,"style":969},"360","384",[141,975],{"x1":976,"y1":967,"x2":977,"y2":967,"stroke":73,"style":969},"546","570",[61,979,981],{"x":157,"y":980,"style":72,"fill":73,"textAnchor":67},"196","A bounding-box pre-filter turns an O(n × m) full scan into a workable two-step query.",[61,983,985],{"x":157,"y":984,"style":72,"fill":73,"textAnchor":67},"220","Both layers must share one CRS, or the box and predicate tests compare meaningless coordinates.",[233,987,989],{"className":235,"code":988,"language":237,"meta":238,"style":238},"from qgis.core import QgsSpatialIndex, QgsVectorLayer\n\n\ndef tag_points_by_zone(points: QgsVectorLayer, zones: QgsVectorLayer,\n                       zone_attr: str = \"zone_name\"):\n    index = QgsSpatialIndex(zones.getFeatures())\n    zone_lookup = {f.id(): f for f in zones.getFeatures()}\n    zone_idx = points.fields().indexFromName(\"zone\")\n\n    from qgis.core import QgsField\n    from qgis.PyQt.QtCore import QVariant\n    if zone_idx == -1:\n        points.dataProvider().addAttributes([QgsField(\"zone\", QVariant.String)])\n        points.updateFields()\n        zone_idx = points.fields().indexFromName(\"zone\")\n\n    with edit(points):\n        for pt in points.getFeatures():\n            geom = pt.geometry()\n            # Narrow to bounding-box candidates, then test the exact predicate\n            for cand_id in index.intersects(geom.boundingBox()):\n                zone = zone_lookup[cand_id]\n                if zone.geometry().contains(geom):\n                    points.changeAttributeValue(\n                        pt.id(), zone_idx, zone[zone_attr])\n                    break\n",[199,990,991,1002,1006,1010,1020,1034,1044,1064,1079,1083,1095,1105,1120,1130,1135,1148,1152,1159,1171,1180,1185,1198,1208,1216,1222,1228],{"__ignoreMap":238},[242,992,993,995,997,999],{"class":141,"line":244},[242,994,248],{"class":247},[242,996,252],{"class":251},[242,998,255],{"class":247},[242,1000,1001],{"class":251}," QgsSpatialIndex, QgsVectorLayer\n",[242,1003,1004],{"class":141,"line":261},[242,1005,265],{"emptyLinePlaceholder":264},[242,1007,1008],{"class":141,"line":268},[242,1009,265],{"emptyLinePlaceholder":264},[242,1011,1012,1014,1017],{"class":141,"line":273},[242,1013,276],{"class":247},[242,1015,1016],{"class":279}," tag_points_by_zone",[242,1018,1019],{"class":251},"(points: QgsVectorLayer, zones: QgsVectorLayer,\n",[242,1021,1022,1025,1027,1029,1032],{"class":141,"line":293},[242,1023,1024],{"class":251},"                       zone_attr: ",[242,1026,287],{"class":286},[242,1028,680],{"class":247},[242,1030,1031],{"class":305}," \"zone_name\"",[242,1033,686],{"class":251},[242,1035,1036,1039,1041],{"class":141,"line":318},[242,1037,1038],{"class":251},"    index ",[242,1040,299],{"class":247},[242,1042,1043],{"class":251}," QgsSpatialIndex(zones.getFeatures())\n",[242,1045,1046,1049,1051,1054,1056,1059,1061],{"class":141,"line":330},[242,1047,1048],{"class":251},"    zone_lookup ",[242,1050,299],{"class":247},[242,1052,1053],{"class":251}," {f.id(): f ",[242,1055,599],{"class":247},[242,1057,1058],{"class":251}," f ",[242,1060,387],{"class":247},[242,1062,1063],{"class":251}," zones.getFeatures()}\n",[242,1065,1066,1069,1071,1074,1077],{"class":141,"line":362},[242,1067,1068],{"class":251},"    zone_idx ",[242,1070,299],{"class":247},[242,1072,1073],{"class":251}," points.fields().indexFromName(",[242,1075,1076],{"class":305},"\"zone\"",[242,1078,315],{"class":251},[242,1080,1081],{"class":141,"line":367},[242,1082,265],{"emptyLinePlaceholder":264},[242,1084,1085,1088,1090,1092],{"class":141,"line":378},[242,1086,1087],{"class":247},"    from",[242,1089,252],{"class":251},[242,1091,255],{"class":247},[242,1093,1094],{"class":251}," QgsField\n",[242,1096,1097,1099,1101,1103],{"class":141,"line":393},[242,1098,1087],{"class":247},[242,1100,652],{"class":251},[242,1102,255],{"class":247},[242,1104,657],{"class":251},[242,1106,1107,1109,1112,1114,1116,1118],{"class":141,"line":404},[242,1108,321],{"class":247},[242,1110,1111],{"class":251}," zone_idx ",[242,1113,701],{"class":247},[242,1115,704],{"class":247},[242,1117,707],{"class":286},[242,1119,435],{"class":251},[242,1121,1122,1125,1127],{"class":141,"line":416},[242,1123,1124],{"class":251},"        points.dataProvider().addAttributes([QgsField(",[242,1126,1076],{"class":305},[242,1128,1129],{"class":251},", QVariant.String)])\n",[242,1131,1132],{"class":141,"line":421},[242,1133,1134],{"class":251},"        points.updateFields()\n",[242,1136,1137,1140,1142,1144,1146],{"class":141,"line":438},[242,1138,1139],{"class":251},"        zone_idx ",[242,1141,299],{"class":247},[242,1143,1073],{"class":251},[242,1145,1076],{"class":305},[242,1147,315],{"class":251},[242,1149,1150],{"class":141,"line":463},[242,1151,265],{"emptyLinePlaceholder":264},[242,1153,1154,1156],{"class":141,"line":471},[242,1155,771],{"class":247},[242,1157,1158],{"class":251}," edit(points):\n",[242,1160,1161,1163,1166,1168],{"class":141,"line":483},[242,1162,779],{"class":247},[242,1164,1165],{"class":251}," pt ",[242,1167,387],{"class":247},[242,1169,1170],{"class":251}," points.getFeatures():\n",[242,1172,1173,1175,1177],{"class":141,"line":488},[242,1174,790],{"class":251},[242,1176,299],{"class":247},[242,1178,1179],{"class":251}," pt.geometry()\n",[242,1181,1182],{"class":141,"line":798},[242,1183,1184],{"class":614},"            # Narrow to bounding-box candidates, then test the exact predicate\n",[242,1186,1187,1190,1193,1195],{"class":141,"line":807},[242,1188,1189],{"class":247},"            for",[242,1191,1192],{"class":251}," cand_id ",[242,1194,387],{"class":247},[242,1196,1197],{"class":251}," index.intersects(geom.boundingBox()):\n",[242,1199,1200,1203,1205],{"class":141,"line":818},[242,1201,1202],{"class":251},"                zone ",[242,1204,299],{"class":247},[242,1206,1207],{"class":251}," zone_lookup[cand_id]\n",[242,1209,1210,1213],{"class":141,"line":835},[242,1211,1212],{"class":247},"                if",[242,1214,1215],{"class":251}," zone.geometry().contains(geom):\n",[242,1217,1219],{"class":141,"line":1218},24,[242,1220,1221],{"class":251},"                    points.changeAttributeValue(\n",[242,1223,1225],{"class":141,"line":1224},25,[242,1226,1227],{"class":251},"                        pt.id(), zone_idx, zone[zone_attr])\n",[242,1229,1231],{"class":141,"line":1230},26,[242,1232,1233],{"class":247},"                    break\n",[15,1235,1236,1237,1240,1241,1244,1245,1248,1249,1251,1252,1256,1257,1260],{},"The index turns a full scan into a two-step filter: ",[199,1238,1239],{},"index.intersects()"," returns cheap bounding-box hits, and only those candidates get the exact, expensive ",[199,1242,1243],{},"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 ",[199,1246,1247],{},"QgsCoordinateTransform"," first when they differ, and see ",[19,1250,215],{"href":214}," for the transform mechanics. When the operation is a hard spatial cut rather than a tag, the dedicated ",[19,1253,1255],{"href":1254},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fclip-vector-layer-pyqgis\u002F","clip a vector layer in PyQGIS"," recipe wraps the ",[199,1258,1259],{},"native:clip"," algorithm and handles the edge cases.",[177,1262,1264],{"id":1263},"cleaning-and-repairing-geometry","Cleaning and Repairing Geometry",[15,1266,1267,1268,1270],{},"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 ",[199,1269,116],{}," method, and stages the survivors in a temporary memory layer before writing them out.",[233,1272,1274],{"className":235,"code":1273,"language":237,"meta":238,"style":238},"from qgis.core import (\n    QgsVectorLayer, QgsFeature, QgsVectorFileWriter, QgsProject,\n)\n\n\ndef filter_and_clean(layer: QgsVectorLayer, min_area_m2: float = 100.0) -> QgsVectorLayer:\n    # Create a memory layer matching the source schema\n    mem_layer = QgsVectorLayer(\n        \"Polygon?crs={}\".format(layer.crs().authid()), \"cleaned_temp\", \"memory\"\n    )\n    mem_layer.dataProvider().addAttributes(layer.fields())\n    mem_layer.updateFields()\n    mem_layer.startEditing()\n\n    for feature in layer.getFeatures():\n        geom = feature.geometry()\n        # Drop slivers below the area threshold\n        if geom.area() \u003C min_area_m2:\n            continue\n        # Repair topology using the modern GEOS method\n        if not geom.isGeosValid():\n            geom = geom.makeValid()\n\n        new_feat = QgsFeature()\n        new_feat.setGeometry(geom)\n        new_feat.setAttributes(feature.attributes())\n        mem_layer.addFeature(new_feat)\n\n    mem_layer.commitChanges()\n    return mem_layer\n",[199,1275,1276,1287,1292,1296,1300,1304,1324,1329,1339,1360,1365,1370,1375,1380,1384,1394,1403,1408,1421,1426,1431,1439,1448,1452,1462,1467,1472,1478,1483,1489],{"__ignoreMap":238},[242,1277,1278,1280,1282,1284],{"class":141,"line":244},[242,1279,248],{"class":247},[242,1281,252],{"class":251},[242,1283,255],{"class":247},[242,1285,1286],{"class":251}," (\n",[242,1288,1289],{"class":141,"line":261},[242,1290,1291],{"class":251},"    QgsVectorLayer, QgsFeature, QgsVectorFileWriter, QgsProject,\n",[242,1293,1294],{"class":141,"line":268},[242,1295,315],{"class":251},[242,1297,1298],{"class":141,"line":273},[242,1299,265],{"emptyLinePlaceholder":264},[242,1301,1302],{"class":141,"line":293},[242,1303,265],{"emptyLinePlaceholder":264},[242,1305,1306,1308,1311,1314,1317,1319,1322],{"class":141,"line":318},[242,1307,276],{"class":247},[242,1309,1310],{"class":279}," filter_and_clean",[242,1312,1313],{"class":251},"(layer: QgsVectorLayer, min_area_m2: ",[242,1315,1316],{"class":286},"float",[242,1318,680],{"class":247},[242,1320,1321],{"class":286}," 100.0",[242,1323,290],{"class":251},[242,1325,1326],{"class":141,"line":330},[242,1327,1328],{"class":614},"    # Create a memory layer matching the source schema\n",[242,1330,1331,1334,1336],{"class":141,"line":362},[242,1332,1333],{"class":251},"    mem_layer ",[242,1335,299],{"class":247},[242,1337,1338],{"class":251}," QgsVectorLayer(\n",[242,1340,1341,1344,1347,1349,1352,1355,1357],{"class":141,"line":367},[242,1342,1343],{"class":305},"        \"Polygon?crs=",[242,1345,1346],{"class":286},"{}",[242,1348,357],{"class":305},[242,1350,1351],{"class":251},".format(layer.crs().authid()), ",[242,1353,1354],{"class":305},"\"cleaned_temp\"",[242,1356,309],{"class":251},[242,1358,1359],{"class":305},"\"memory\"\n",[242,1361,1362],{"class":141,"line":378},[242,1363,1364],{"class":251},"    )\n",[242,1366,1367],{"class":141,"line":393},[242,1368,1369],{"class":251},"    mem_layer.dataProvider().addAttributes(layer.fields())\n",[242,1371,1372],{"class":141,"line":404},[242,1373,1374],{"class":251},"    mem_layer.updateFields()\n",[242,1376,1377],{"class":141,"line":416},[242,1378,1379],{"class":251},"    mem_layer.startEditing()\n",[242,1381,1382],{"class":141,"line":421},[242,1383,265],{"emptyLinePlaceholder":264},[242,1385,1386,1388,1390,1392],{"class":141,"line":438},[242,1387,381],{"class":247},[242,1389,384],{"class":251},[242,1391,387],{"class":247},[242,1393,390],{"class":251},[242,1395,1396,1399,1401],{"class":141,"line":463},[242,1397,1398],{"class":251},"        geom ",[242,1400,299],{"class":247},[242,1402,795],{"class":251},[242,1404,1405],{"class":141,"line":471},[242,1406,1407],{"class":614},"        # Drop slivers below the area threshold\n",[242,1409,1410,1412,1415,1418],{"class":141,"line":483},[242,1411,396],{"class":247},[242,1413,1414],{"class":251}," geom.area() ",[242,1416,1417],{"class":247},"\u003C",[242,1419,1420],{"class":251}," min_area_m2:\n",[242,1422,1423],{"class":141,"line":488},[242,1424,1425],{"class":247},"            continue\n",[242,1427,1428],{"class":141,"line":798},[242,1429,1430],{"class":614},"        # Repair topology using the modern GEOS method\n",[242,1432,1433,1435,1437],{"class":141,"line":807},[242,1434,396],{"class":247},[242,1436,324],{"class":247},[242,1438,804],{"class":251},[242,1440,1441,1443,1445],{"class":141,"line":818},[242,1442,790],{"class":251},[242,1444,299],{"class":247},[242,1446,1447],{"class":251}," geom.makeValid()\n",[242,1449,1450],{"class":141,"line":835},[242,1451,265],{"emptyLinePlaceholder":264},[242,1453,1454,1457,1459],{"class":141,"line":1218},[242,1455,1456],{"class":251},"        new_feat ",[242,1458,299],{"class":247},[242,1460,1461],{"class":251}," QgsFeature()\n",[242,1463,1464],{"class":141,"line":1224},[242,1465,1466],{"class":251},"        new_feat.setGeometry(geom)\n",[242,1468,1469],{"class":141,"line":1230},[242,1470,1471],{"class":251},"        new_feat.setAttributes(feature.attributes())\n",[242,1473,1475],{"class":141,"line":1474},27,[242,1476,1477],{"class":251},"        mem_layer.addFeature(new_feat)\n",[242,1479,1481],{"class":141,"line":1480},28,[242,1482,265],{"emptyLinePlaceholder":264},[242,1484,1486],{"class":141,"line":1485},29,[242,1487,1488],{"class":251},"    mem_layer.commitChanges()\n",[242,1490,1492,1494],{"class":141,"line":1491},30,[242,1493,491],{"class":247},[242,1495,1496],{"class":251}," mem_layer\n",[15,1498,1499,1501,1502,1505,1506,1509,1510,1514,1515,1519],{},[199,1500,116],{}," is the GEOS-backed standard for resolving self-intersecting polygons while preserving their structure and attributes. Prefer it over the legacy zero-buffer trick (",[199,1503,1504],{},"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 ",[199,1507,1508],{},"native:fixgeometries",", which you can drop straight into a pipeline — see ",[19,1511,1513],{"href":1512},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms in PyQGIS"," for how to wire it between other steps, and ",[19,1516,1518],{"href":1517},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002F","Batch Processing with PyQGIS"," for running it over a folder of layers.",[177,1521,1523],{"id":1522},"exporting-and-converting-formats","Exporting and Converting Formats",[15,1525,1526,1527,1530,1531,1534],{},"The final stage serializes the processed layer into a standardized format for downstream consumption. ",[199,1528,1529],{},"writeAsVectorFormatV3"," is the current writer: it exposes the ",[199,1532,1533],{},"SaveVectorOptions"," API for driver selection, encoding, and CRS transforms, and returns a clear error tuple you can assert on.",[233,1536,1538],{"className":235,"code":1537,"language":237,"meta":238,"style":238},"from qgis.core import QgsVectorFileWriter, QgsProject\n\n\ndef export_layer(layer, output_path: str, driver: str = \"GPKG\"):\n    options = QgsVectorFileWriter.SaveVectorOptions()\n    options.driverName = driver\n    error_code, error_msg, _, _ = QgsVectorFileWriter.writeAsVectorFormatV3(\n        layer, output_path, QgsProject.instance().transformContext(), options\n    )\n    if error_code != QgsVectorFileWriter.NoError:\n        raise RuntimeError(f\"Export failed: {error_msg}\")\n    return output_path\n",[199,1539,1540,1551,1555,1559,1583,1593,1603,1613,1618,1622,1635,1660],{"__ignoreMap":238},[242,1541,1542,1544,1546,1548],{"class":141,"line":244},[242,1543,248],{"class":247},[242,1545,252],{"class":251},[242,1547,255],{"class":247},[242,1549,1550],{"class":251}," QgsVectorFileWriter, QgsProject\n",[242,1552,1553],{"class":141,"line":261},[242,1554,265],{"emptyLinePlaceholder":264},[242,1556,1557],{"class":141,"line":268},[242,1558,265],{"emptyLinePlaceholder":264},[242,1560,1561,1563,1566,1569,1571,1574,1576,1578,1581],{"class":141,"line":273},[242,1562,276],{"class":247},[242,1564,1565],{"class":279}," export_layer",[242,1567,1568],{"class":251},"(layer, output_path: ",[242,1570,287],{"class":286},[242,1572,1573],{"class":251},", driver: ",[242,1575,287],{"class":286},[242,1577,680],{"class":247},[242,1579,1580],{"class":305}," \"GPKG\"",[242,1582,686],{"class":251},[242,1584,1585,1588,1590],{"class":141,"line":293},[242,1586,1587],{"class":251},"    options ",[242,1589,299],{"class":247},[242,1591,1592],{"class":251}," QgsVectorFileWriter.SaveVectorOptions()\n",[242,1594,1595,1598,1600],{"class":141,"line":318},[242,1596,1597],{"class":251},"    options.driverName ",[242,1599,299],{"class":247},[242,1601,1602],{"class":251}," driver\n",[242,1604,1605,1608,1610],{"class":141,"line":330},[242,1606,1607],{"class":251},"    error_code, error_msg, _, _ ",[242,1609,299],{"class":247},[242,1611,1612],{"class":251}," QgsVectorFileWriter.writeAsVectorFormatV3(\n",[242,1614,1615],{"class":141,"line":362},[242,1616,1617],{"class":251},"        layer, output_path, QgsProject.instance().transformContext(), options\n",[242,1619,1620],{"class":141,"line":367},[242,1621,1364],{"class":251},[242,1623,1624,1626,1629,1632],{"class":141,"line":378},[242,1625,321],{"class":247},[242,1627,1628],{"class":251}," error_code ",[242,1630,1631],{"class":247},"!=",[242,1633,1634],{"class":251}," QgsVectorFileWriter.NoError:\n",[242,1636,1637,1639,1642,1644,1646,1649,1651,1654,1656,1658],{"class":141,"line":393},[242,1638,333],{"class":247},[242,1640,1641],{"class":286}," RuntimeError",[242,1643,339],{"class":251},[242,1645,342],{"class":247},[242,1647,1648],{"class":305},"\"Export failed: ",[242,1650,348],{"class":286},[242,1652,1653],{"class":251},"error_msg",[242,1655,354],{"class":286},[242,1657,357],{"class":305},[242,1659,315],{"class":251},[242,1661,1662,1664],{"class":141,"line":404},[242,1663,491],{"class":247},[242,1665,1666],{"class":251}," output_path\n",[15,1668,1669,1670,1673,1674,1677,1678,1682,1683,1687,1688,525],{},"Choose the driver to fit the consumer: ",[199,1671,1672],{},"\"GPKG\""," for a robust, multi-layer working format, or ",[199,1675,1676],{},"\"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 ",[19,1679,1681],{"href":1680},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fautomating-shapefile-to-geojson-conversion\u002F","automating shapefile to GeoJSON conversion"," guide covers precision control and encoding pitfalls. When you instead need to fold many inputs into one output, ",[19,1684,1686],{"href":1685},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fmerge-multiple-shapefiles-pyqgis\u002F","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 ",[19,1689,1691],{"href":1690},"\u002Fspatial-data-processing-automation\u002Fraster-analysis-workflows\u002F","Raster Analysis Workflows in PyQGIS",[177,1693,1695],{"id":1694},"common-errors-and-troubleshooting","Common Errors and Troubleshooting",[15,1697,1698],{},"Programmatic vector manipulation introduces several failure points that are easily mitigated with defensive coding.",[15,1700,1701,1706,1707,1710,1711,1714,1715,1718],{},[191,1702,1703,1705],{},[199,1704,74],{}," fails to initialize."," ",[228,1708,1709],{},"Cause:"," incorrect provider string, missing file permissions, or an unsupported format. ",[228,1712,1713],{},"Fix:"," confirm the OGR driver supports the input, and use the three-argument constructor ",[199,1716,1717],{},"QgsVectorLayer(path, \"name\", \"ogr\")",". Legacy CAD files (DXF\u002FDWG) often need preprocessing before PyQGIS can parse their geometry.",[15,1720,1721,1706,1724,1726,1727,1729,1730,1733,1734,1736,1737,1739],{},[191,1722,1723],{},"CRS mismatch during spatial operations.",[228,1725,1709],{}," running distance\u002Farea calculations or joins on layers with differing coordinate systems. ",[228,1728,1713],{}," verify ",[199,1731,1732],{},"layer.crs().isValid()"," and that both operands share a CRS; align them with ",[199,1735,1247],{},", or rely on ",[199,1738,102],{}," for measurements, which handles ellipsoidal math without physical reprojection.",[15,1741,1742,1706,1748,1750,1751,1753,1754,1756,1757,309,1760,1763,1764,1767],{},[191,1743,1744,1747],{},[199,1745,1746],{},"MemoryError"," on large datasets.",[228,1749,1709],{}," loading whole layers into memory or iterating without chunking. ",[228,1752,1713],{}," use ",[199,1755,531],{}," with ",[199,1758,1759],{},"setLimit()",[199,1761,1762],{},"setFilterExpression()",", or ",[199,1765,1766],{},"setSubsetOfAttributes()"," to subset data, write to a file-based GeoPackage instead of a memory layer, and process in spatial tiles for enterprise-scale jobs.",[15,1769,1770,1706,1773,1775,1776,1778,1779,1782,1783,309,1786,1789],{},[191,1771,1772],{},"Attribute update appears to succeed but never persists.",[228,1774,1709],{}," edits made outside an edit session, or a read-only source. ",[228,1777,1713],{}," wrap changes in ",[199,1780,1781],{},"with edit(layer):",", confirm the provider is writable (GeoPackage and PostGIS are; some are not), and ensure Shapefile sidecars (",[199,1784,1785],{},".dbf",[199,1787,1788],{},".shx",") have write permission.",[177,1791,1793],{"id":1792},"choosing-your-level-of-abstraction","Choosing your level of abstraction",[15,1795,1796],{},"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.",[15,1798,1799],{},[28,1800,1802,1805,1808,1810,1818,1823,1829,1834,1855,1858,1861,1875,1878,1882,1896,1902,1907],{"viewBox":878,"role":31,"ariaLabel":1801,"xmlns":33},"Three levels for the same vector task: a Processing algorithm, a feature loop with the provider API, and geometry-level operations, ordered by control and by effort",[35,1803,1804],{},"Three levels of abstraction for one task",[39,1806,1807],{},"A Processing algorithm handles a whole layer in one call with progress and error handling built in. A feature loop through the provider API gives per-feature control at the cost of writing the iteration and the output yourself. Geometry-level operations give complete control and require you to build everything around them. Effort rises and abstraction falls from left to right.",[43,1809],{"x":888,"y":888,"width":889,"height":890,"fill":49},[161,1811,1812],{},[164,1813,1815],{"id":1814,"viewBox":957,"refX":958,"refY":959,"markerWidth":168,"markerHeight":168,"orient":960},"vecLevelArrow",[173,1816],{"d":963,"fill":1817},"#59645f",[61,1819,1822],{"x":157,"y":1820,"style":1821,"fill":66,"textAnchor":67},"26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","Start at the top and drop down only when you must",[43,1824],{"x":1825,"y":1826,"width":1827,"height":1828,"rx":56,"fill":57,"stroke":124,"style":59},"16","46","236","146",[61,1830,1833],{"x":1831,"y":53,"style":1832,"fill":124,"textAnchor":67},"134","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","processing.run()",[1835,1836,1838,1843,1847,1851],"g",{"style":1837},"font-size:10.5px;font-family:sans-serif",[61,1839,1842],{"x":1831,"y":1840,"style":1841,"fill":73,"textAnchor":67},"98","text-anchor:middle","one call, whole layer",[61,1844,1846],{"x":1831,"y":1845,"style":1841,"fill":73,"textAnchor":67},"120","progress and errors handled",[61,1848,1850],{"x":1831,"y":1849,"style":1841,"fill":73,"textAnchor":67},"142","writes the output for you",[61,1852,1854],{"x":1831,"y":1853,"style":1841,"fill":124,"textAnchor":67},"170","use this by default",[43,1856],{"x":1857,"y":1826,"width":1827,"height":1828,"rx":56,"fill":57,"stroke":86,"style":59},"262",[61,1859,1860],{"x":157,"y":53,"style":1832,"fill":86,"textAnchor":67},"a feature loop",[1835,1862,1863,1866,1869,1872],{"style":1837},[61,1864,1865],{"x":157,"y":1840,"style":1841,"fill":73,"textAnchor":67},"per-feature decisions",[61,1867,1868],{"x":157,"y":1845,"style":1841,"fill":73,"textAnchor":67},"you write the iteration",[61,1870,1871],{"x":157,"y":1849,"style":1841,"fill":73,"textAnchor":67},"and the output",[61,1873,1874],{"x":157,"y":1853,"style":1841,"fill":86,"textAnchor":67},"when the logic branches",[43,1876],{"x":1877,"y":1826,"width":1827,"height":1828,"rx":56,"fill":57,"stroke":106,"style":59},"508",[61,1879,1881],{"x":1880,"y":53,"style":1832,"fill":106,"textAnchor":67},"626","QgsGeometry calls",[1835,1883,1884,1887,1890,1893],{"style":1837},[61,1885,1886],{"x":1880,"y":1840,"style":1841,"fill":73,"textAnchor":67},"complete control",[61,1888,1889],{"x":1880,"y":1845,"style":1841,"fill":73,"textAnchor":67},"nothing done for you",[61,1891,1892],{"x":1880,"y":1849,"style":1841,"fill":73,"textAnchor":67},"no intermediate files",[61,1894,1895],{"x":1880,"y":1853,"style":1841,"fill":106,"textAnchor":67},"when no algorithm fits",[141,1897],{"x1":1898,"y1":1899,"x2":1900,"y2":1899,"stroke":1817,"style":1901},"40","216","720","stroke-width:2;marker-end:url(#vecLevelArrow)",[61,1903,1906],{"x":1898,"y":1904,"style":1905,"fill":124},"238","font-size:11px;font-family:sans-serif","less code",[61,1908,1911],{"x":1900,"y":1904,"style":1909,"fill":106,"textAnchor":1910},"text-anchor:end;font-size:11px;font-family:sans-serif","end","more control",[15,1913,1914,1915,1918],{},"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 ",[199,1916,1917],{},"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.",[15,1920,1921],{},"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\u002FO.",[177,1923,1925],{"id":1924},"key-takeaways","Key Takeaways",[185,1927,1928,1940,1949,1958,1981],{},[188,1929,1930,1933,1934,1936,1937,1939],{},[191,1931,1932],{},"Validate before you manipulate."," Separate ",[199,1935,78],{}," (can the source open?) from per-feature ",[199,1938,82],{}," (is this geometry safe to process?) and repair problems at ingestion, not at export.",[188,1941,1942,1945,1946,1948],{},[191,1943,1944],{},"Units follow the CRS."," Use ",[199,1947,102],{}," for 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.",[188,1950,1951,1954,1955,1957],{},[191,1952,1953],{},"Index your spatial joins."," A ",[199,1956,874],{}," bounding-box pre-filter turns an unusable O(n×m) scan into a workable two-step query.",[188,1959,1960,1706,1963,1965,1966,309,1969,1971,1972,1974,1975,829,1978,525],{},[191,1961,1962],{},"Prefer modern APIs.",[199,1964,116],{}," over ",[199,1967,1968],{},"buffer(0)",[199,1970,1529],{}," over deprecated writers, and the ",[199,1973,846],{}," context manager over manual ",[199,1976,1977],{},"startEditing()",[199,1979,1980],{},"commitChanges()",[188,1982,1983,1986],{},[191,1984,1985],{},"Keep each stage modular."," Load, edit, join, clean, and export as separate functions so they drop straight into chained pipelines and batch loops.",[177,1988,1990],{"id":1989},"frequently-asked-questions","Frequently Asked Questions",[15,1992,1993,1999,2000,2002],{},[191,1994,1995,1996,1998],{},"Why should I validate geometries with ",[199,1997,82],{}," before manipulating a vector layer?","\nInvalid geometries — self-intersections, duplicate nodes, or unclosed rings — cause spatial predicates and overlay operations to return unpredictable results or drop features silently. Checking ",[199,2001,82],{}," 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.",[15,2004,2005,2011,2013,2014,2016,2017,2019,2020,2022],{},[191,2006,2007,2008,2010],{},"What is the difference between ",[199,2009,116],{}," and the old zero-buffer trick for fixing polygons?",[199,2012,116],{}," is the modern GEOS-backed method that repairs self-intersecting or malformed polygons while preserving their structure and attributes. The legacy zero-buffer workaround (",[199,2015,1504],{},") often collapses slivers, merges rings unpredictably, and can distort valid geometry. On QGIS 3.34 LTR you should always prefer ",[199,2018,116],{},", or the ",[199,2021,1508],{}," algorithm for batch jobs.",[15,2024,2025,2031,2032,2034,2035,2037,2038,2041],{},[191,2026,2027,2028,2030],{},"Should I use ",[199,2029,1529],{}," or the older writer methods for exporting?","\nUse ",[199,2033,1529],{}," on any modern QGIS install (3.16+, including 3.34 LTR). It exposes the ",[199,2036,1533],{}," 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 ",[199,2039,2040],{},"writeAsVectorFormat"," is deprecated and lacks transactional safety.",[15,2043,2044,2047,2048,2050,2051,2054,2055,309,2058,309,2061,2064],{},[191,2045,2046],{},"How do I make spatial joins fast on large layers?","\nBuild a ",[199,2049,874],{}," over the reference layer, then use ",[199,2052,2053],{},"index.intersects(geom.boundingBox())"," to shortlist candidates before running the exact predicate (",[199,2056,2057],{},"contains",[199,2059,2060],{},"intersects",[199,2062,2063],{},"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.",[15,2066,2067,2073,2074,1756,2076,309,2078,1763,2080,2082],{},[191,2068,2069,2070,2072],{},"How do I avoid ",[199,2071,1746],{}," when manipulating very large vector layers?","\nDo not load entire layers into memory at once. Use ",[199,2075,531],{},[199,2077,1762],{},[199,2079,1759],{},[199,2081,1766],{}," 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.",[15,2084,2085,2088,2089,2091,2092,2095,2096,829,2098,2100],{},[191,2086,2087],{},"Why does my attribute update appear to succeed but the values never persist?","\nThis almost always means the edits were never committed. Wrap changes in the ",[199,2090,1781],{}," context manager, which opens and commits the edit session automatically, or call ",[199,2093,2094],{},"layer.commitChanges()"," explicitly. Also confirm the data source is writable — Shapefiles need write permission on the ",[199,2097,1785],{},[199,2099,1788],{},", and some providers are read-only.",[15,2102,2103,2106],{},[191,2104,2105],{},"Should I use a Processing algorithm or a feature loop?","\nThe 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.",[15,2108,2109,2112,2113,2116,2117,2119,2120,2122],{},[191,2110,2111],{},"Why is my feature loop so slow?","\nUsually because it reads more than it needs. Add ",[199,2114,2115],{},"QgsFeatureRequest.NoGeometry"," when the geometry is not used, restrict the columns with ",[199,2118,1766],{},", and push any filtering into ",[199,2121,1762],{}," so the provider does it rather than Python.",[15,2124,2125,2128],{},[191,2126,2127],{},"What is the safest output format?","\nGeoPackage. 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.",[15,2130,2131,2134,2137,2138,2141,2142,2144],{},[191,2132,2133],{},"How do I check a layer loaded correctly?",[199,2135,2136],{},"layer.isValid()",". An invalid layer is not ",[199,2139,2140],{},"None"," — it is a real object that returns zero features and an empty extent, so code that only checks for ",[199,2143,2140],{}," proceeds happily on nothing at all.",[15,2146,2147,2150],{},[191,2148,2149],{},"Does deleting features renumber the remaining feature ids?","\nNo — identifiers are provider-assigned and stable for a given feature.",[177,2152,2154],{"id":2153},"related","Related",[185,2156,2157,2162,2167,2171,2175,2179,2184,2189,2194,2199,2205,2211],{},[188,2158,2159,2161],{},[19,2160,22],{"href":21}," — the parent overview this guide belongs to",[188,2163,2164],{},[19,2165,2166],{"href":214},"Coordinate Reference Systems in PyQGIS",[188,2168,2169],{},[19,2170,1691],{"href":1690},[188,2172,2173],{},[19,2174,1513],{"href":1512},[188,2176,2177],{},[19,2178,1518],{"href":1517},[188,2180,2181],{},[19,2182,2183],{"href":850},"PyQGIS Script to Calculate Polygon Areas",[188,2185,2186],{},[19,2187,2188],{"href":1680},"Automating Shapefile to GeoJSON Conversion in QGIS",[188,2190,2191],{},[19,2192,2193],{"href":1254},"Clip a Vector Layer in PyQGIS",[188,2195,2196],{},[19,2197,2198],{"href":1685},"Merge Multiple Shapefiles in PyQGIS",[188,2200,2201],{},[19,2202,2204],{"href":2203},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fdissolve-features-by-attribute-pyqgis\u002F","Dissolve Features by Attribute in PyQGIS",[188,2206,2207],{},[19,2208,2210],{"href":2209},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fspatial-join-points-in-polygons-pyqgis\u002F","Spatial Join Points to Polygons in PyQGIS",[188,2212,2213],{},[19,2214,2216],{"href":2215},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002F","PostGIS and Database Workflows in PyQGIS",[2218,2219,2220],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":238,"searchDepth":261,"depth":261,"links":2222},[2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233],{"id":179,"depth":261,"text":180},{"id":222,"depth":261,"text":223},{"id":618,"depth":261,"text":619},{"id":859,"depth":261,"text":860},{"id":1263,"depth":261,"text":1264},{"id":1522,"depth":261,"text":1523},{"id":1694,"depth":261,"text":1695},{"id":1792,"depth":261,"text":1793},{"id":1924,"depth":261,"text":1925},{"id":1989,"depth":261,"text":1990},{"id":2153,"depth":261,"text":2154},"Manipulate vector data in QGIS with PyQGIS — load and validate layers, edit attributes, run spatial joins and filters, repair geometry, and convert formats.","md",{"slug":2237,"type":2238,"breadcrumb":2239,"datePublished":2240,"dateModified":2241},"vector-data-manipulation","guide","Vector Data Manipulation","2025-03-18","2026-07-18","\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation",{"title":5,"description":2234},"spatial-data-processing-automation\u002Fvector-data-manipulation\u002Findex","Yov6vldkvWaFu2RwUJrp7GtbEDjIO0MkKZKEQvVAyI4",1787823364682]