[{"data":1,"prerenderedAt":2809},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management":3},{"id":4,"title":5,"body":6,"description":2798,"extension":2799,"meta":2800,"navigation":293,"path":2805,"seo":2806,"stem":2807,"__hash__":2808},"docs\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Findex.md","Attribute Tables and Field Management in PyQGIS",{"type":7,"value":8,"toc":2783},"minimark",[9,13,17,26,199,204,239,243,261,477,494,497,552,568,572,575,705,742,745,846,864,868,871,1039,1073,1080,1162,1183,1187,1194,1322,1355,1428,1434,1438,1441,1549,1577,1581,1584,1717,1854,1886,1890,1897,2052,2082,2086,2093,2261,2286,2290,2293,2311,2324,2344,2549,2564,2586,2590,2659,2663,2677,2689,2698,2716,2725,2729,2779],[10,11,5],"h1",{"id":12},"attribute-tables-and-field-management-in-pyqgis",[14,15,16],"p",{},"Geometry gets the attention, but most automation work is attribute work: adding a calculated column, normalising a category, joining a spreadsheet of measurements onto a set of polygons, or exporting a summary for a report. PyQGIS gives you two quite different routes into the attribute table — the provider API, which writes straight to the data source, and the edit buffer, which stages changes so they can be undone — and knowing which one you are using is the difference between a script that is safe to re-run and one that corrupts a dataset halfway through.",[14,18,19,20,25],{},"This guide sits inside ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002F","Spatial Data Processing & Automation",". It covers the field model, adding and deleting fields, bulk value updates, attribute joins, aggregation, and export — with an emphasis on the transactional behaviour that decides whether a partial failure leaves your data intact.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,93,98,106,110,114,118,122,126,130,134,138,143,148,152,157,163,167,171,175,183,187,192,196],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 268","img","The two routes for editing attributes: the provider API writes directly to the data source, while the edit buffer stages changes that must be committed or rolled back","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Provider writes versus the edit buffer",[40,41,42],"desc",{},"A PyQGIS script can take two paths to change attributes. The provider path calls dataProvider methods that write immediately to the file or database with no undo. The edit buffer path calls startEditing, stages changes in memory, and ends in either commitChanges, which writes them, or rollBack, which discards them.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","268","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"attrRouteArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Two routes into the attribute table — only one of them is undoable",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"16","106","130","60","10","#26322d","#0f766e","stroke-width:2.5",[68,87,92],{"x":88,"y":89,"style":90,"fill":91,"textAnchor":74},"81","132","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","#d9f99d","your script",[68,94,97],{"x":88,"y":95,"style":96,"fill":91,"textAnchor":74},"152","text-anchor:middle;font-size:11.5px;font-family:sans-serif","PyQGIS",[44,99],{"x":100,"y":101,"width":102,"height":103,"rx":82,"fill":104,"stroke":105,"style":85},"212","46","230","72","#fffdf7","#b45309",[68,107,109],{"x":108,"y":103,"style":90,"fill":105,"textAnchor":74},"327","provider · dataProvider()",[68,111,113],{"x":108,"y":112,"style":96,"fill":66,"textAnchor":74},"93","addAttributes · changeAttributeValues",[68,115,117],{"x":108,"y":116,"style":96,"fill":105,"textAnchor":74},"110","writes immediately · no undo",[44,119],{"x":100,"y":95,"width":102,"height":120,"rx":82,"fill":104,"stroke":121,"style":85},"94","#2563eb",[68,123,125],{"x":108,"y":124,"style":90,"fill":121,"textAnchor":74},"178","edit buffer · startEditing()",[68,127,129],{"x":108,"y":128,"style":96,"fill":66,"textAnchor":74},"199","addAttribute · changeAttributeValue",[68,131,133],{"x":108,"y":132,"style":96,"fill":66,"textAnchor":74},"216","staged in memory",[68,135,137],{"x":108,"y":136,"style":96,"fill":121,"textAnchor":74},"235","undoable until committed",[44,139],{"x":140,"y":101,"width":141,"height":103,"rx":82,"fill":104,"stroke":142,"style":85},"508","236","#59645f",[68,144,147],{"x":145,"y":146,"style":90,"fill":73,"textAnchor":74},"626","80","the data source",[68,149,151],{"x":145,"y":150,"style":96,"fill":66,"textAnchor":74},"101","GeoPackage · PostGIS · shapefile",[44,153],{"x":140,"y":95,"width":154,"height":155,"rx":58,"fill":104,"stroke":156,"style":85},"112","44","#15803d",[68,158,162],{"x":159,"y":160,"style":161,"fill":156,"textAnchor":74},"564","179","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","commitChanges()",[44,164],{"x":165,"y":95,"width":154,"height":155,"rx":58,"fill":104,"stroke":166,"style":85},"632","#b91c1c",[68,168,170],{"x":169,"y":160,"style":161,"fill":166,"textAnchor":74},"688","rollBack()",[68,172,174],{"x":145,"y":173,"style":96,"fill":142,"textAnchor":74},"228","every startEditing() must reach one of these",[176,177],"line",{"x1":178,"y1":179,"x2":180,"y2":181,"stroke":66,"style":182},"146","120","208","90","stroke-width:2.5;marker-end:url(#attrRouteArrow)",[176,184],{"x1":178,"y1":185,"x2":180,"y2":186,"stroke":66,"style":182},"150","182",[176,188],{"x1":189,"y1":190,"x2":191,"y2":190,"stroke":105,"style":182},"442","82","504",[176,193],{"x1":189,"y1":194,"x2":191,"y2":195,"stroke":121,"style":182},"180","174",[176,197],{"x1":159,"y1":95,"x2":159,"y2":198,"stroke":156,"style":182},"122",[200,201,203],"h2",{"id":202},"prerequisites","Prerequisites",[205,206,207,215,223,231],"ul",{},[208,209,210,214],"li",{},[211,212,213],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[208,216,217,218,222],{},"A writable vector layer. GeoPackage is strongly preferred over shapefile: shapefiles truncate field names to ten characters, cannot store ",[219,220,221],"code",{},"NULL"," in every type, and have no real date type.",[208,224,225,226,230],{},"Familiarity with the Python Console — see ",[21,227,229],{"href":228},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002F","QGIS Python Console Basics",".",[208,232,233,234,238],{},"Optionally, an understanding of ",[21,235,237],{"href":236},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002F","QGIS expressions",", which are the shortest path to a calculated column.",[200,240,242],{"id":241},"the-field-model","The field model",[14,244,245,246,249,250,253,254,257,258,230],{},"A layer's schema is a ",[219,247,248],{},"QgsFields"," collection of ",[219,251,252],{},"QgsField"," objects. Each field has a name, a type, and — depending on the provider — a length and precision. The two things worth internalising are that ",[211,255,256],{},"field indexes are positional and shift when you delete a field",", and that ",[211,259,260],{},"not every provider supports every operation",[262,263,268],"pre",{"className":264,"code":265,"language":266,"meta":267,"style":267},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsVectorDataProvider\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nfor field in layer.fields():\n    print(f\"{field.name():\u003C20} {field.typeName():\u003C10} len={field.length()}\")\n\ncapabilities = layer.dataProvider().capabilities()\nprint(\"can add fields:\", bool(capabilities & QgsVectorDataProvider.AddAttributes))\nprint(\"can delete fields:\", bool(capabilities & QgsVectorDataProvider.DeleteAttributes))\nprint(\"can change values:\", bool(capabilities & QgsVectorDataProvider.ChangeAttributeValues))\n","python","",[219,269,270,288,295,320,325,340,393,398,409,435,456],{"__ignoreMap":267},[271,272,274,278,282,285],"span",{"class":176,"line":273},1,[271,275,277],{"class":276},"snl16","from",[271,279,281],{"class":280},"s95oV"," qgis.core ",[271,283,284],{"class":276},"import",[271,286,287],{"class":280}," QgsProject, QgsVectorDataProvider\n",[271,289,291],{"class":176,"line":290},2,[271,292,294],{"emptyLinePlaceholder":293},true,"\n",[271,296,298,301,304,307,311,314,317],{"class":176,"line":297},3,[271,299,300],{"class":280},"layer ",[271,302,303],{"class":276},"=",[271,305,306],{"class":280}," QgsProject.instance().mapLayersByName(",[271,308,310],{"class":309},"sU2Wk","\"parcels\"",[271,312,313],{"class":280},")[",[271,315,46],{"class":316},"sDLfK",[271,318,319],{"class":280},"]\n",[271,321,323],{"class":176,"line":322},4,[271,324,294],{"emptyLinePlaceholder":293},[271,326,328,331,334,337],{"class":176,"line":327},5,[271,329,330],{"class":276},"for",[271,332,333],{"class":280}," field ",[271,335,336],{"class":276},"in",[271,338,339],{"class":280}," layer.fields():\n",[271,341,343,346,349,352,355,358,361,364,367,370,373,376,378,381,383,386,388,390],{"class":176,"line":342},6,[271,344,345],{"class":316},"    print",[271,347,348],{"class":280},"(",[271,350,351],{"class":276},"f",[271,353,354],{"class":309},"\"",[271,356,357],{"class":316},"{",[271,359,360],{"class":280},"field.name()",[271,362,363],{"class":276},":\u003C20",[271,365,366],{"class":316},"}",[271,368,369],{"class":316}," {",[271,371,372],{"class":280},"field.typeName()",[271,374,375],{"class":276},":\u003C10",[271,377,366],{"class":316},[271,379,380],{"class":309}," len=",[271,382,357],{"class":316},[271,384,385],{"class":280},"field.length()",[271,387,366],{"class":316},[271,389,354],{"class":309},[271,391,392],{"class":280},")\n",[271,394,396],{"class":176,"line":395},7,[271,397,294],{"emptyLinePlaceholder":293},[271,399,401,404,406],{"class":176,"line":400},8,[271,402,403],{"class":280},"capabilities ",[271,405,303],{"class":276},[271,407,408],{"class":280}," layer.dataProvider().capabilities()\n",[271,410,412,415,417,420,423,426,429,432],{"class":176,"line":411},9,[271,413,414],{"class":316},"print",[271,416,348],{"class":280},[271,418,419],{"class":309},"\"can add fields:\"",[271,421,422],{"class":280},", ",[271,424,425],{"class":316},"bool",[271,427,428],{"class":280},"(capabilities ",[271,430,431],{"class":276},"&",[271,433,434],{"class":280}," QgsVectorDataProvider.AddAttributes))\n",[271,436,438,440,442,445,447,449,451,453],{"class":176,"line":437},10,[271,439,414],{"class":316},[271,441,348],{"class":280},[271,443,444],{"class":309},"\"can delete fields:\"",[271,446,422],{"class":280},[271,448,425],{"class":316},[271,450,428],{"class":280},[271,452,431],{"class":276},[271,454,455],{"class":280}," QgsVectorDataProvider.DeleteAttributes))\n",[271,457,459,461,463,466,468,470,472,474],{"class":176,"line":458},11,[271,460,414],{"class":316},[271,462,348],{"class":280},[271,464,465],{"class":309},"\"can change values:\"",[271,467,422],{"class":280},[271,469,425],{"class":316},[271,471,428],{"class":280},[271,473,431],{"class":276},[271,475,476],{"class":280}," QgsVectorDataProvider.ChangeAttributeValues))\n",[14,478,479,482,483,486,487,489,490,493],{},[211,480,481],{},"Breakdown:"," Iterating ",[219,484,485],{},"layer.fields()"," yields ",[219,488,252],{}," objects in schema order. ",[219,491,492],{},"capabilities()"," returns a bit flag; testing it before attempting an edit turns a confusing silent failure into an explicit, early one. A read-only WFS layer or a delimited-text layer will fail these checks, and there is no point discovering that halfway through a loop.",[14,495,496],{},"Because indexes shift, always resolve a field by name at the moment you need it rather than caching a number across a schema change:",[262,498,500],{"className":264,"code":499,"language":266,"meta":267,"style":267},"index = layer.fields().indexOf(\"assessed_value\")\nif index == -1:\n    raise KeyError(\"field 'assessed_value' does not exist\")\n",[219,501,502,517,537],{"__ignoreMap":267},[271,503,504,507,509,512,515],{"class":176,"line":273},[271,505,506],{"class":280},"index ",[271,508,303],{"class":276},[271,510,511],{"class":280}," layer.fields().indexOf(",[271,513,514],{"class":309},"\"assessed_value\"",[271,516,392],{"class":280},[271,518,519,522,525,528,531,534],{"class":176,"line":290},[271,520,521],{"class":276},"if",[271,523,524],{"class":280}," index ",[271,526,527],{"class":276},"==",[271,529,530],{"class":276}," -",[271,532,533],{"class":316},"1",[271,535,536],{"class":280},":\n",[271,538,539,542,545,547,550],{"class":176,"line":297},[271,540,541],{"class":276},"    raise",[271,543,544],{"class":316}," KeyError",[271,546,348],{"class":280},[271,548,549],{"class":309},"\"field 'assessed_value' does not exist\"",[271,551,392],{"class":280},[14,553,554,556,557,560,561,564,565,567],{},[211,555,481],{}," ",[219,558,559],{},"indexOf()"," returns ",[219,562,563],{},"-1"," rather than raising when the field is missing, which is easy to miss. Converting that into an explicit exception is nearly always what you want, because passing ",[219,566,563],{}," to a change call silently does nothing.",[200,569,571],{"id":570},"adding-and-removing-fields","Adding and removing fields",[14,573,574],{},"Adding a field goes through the provider, and the layer's own field cache has to be refreshed afterwards — the step that is most often forgotten, producing a script that adds a column and then insists it does not exist.",[262,576,578],{"className":264,"code":577,"language":266,"meta":267,"style":267},"from qgis.core import QgsField, QgsProject\nfrom qgis.PyQt.QtCore import QVariant\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nprovider = layer.dataProvider()\n\nif layer.fields().indexOf(\"value_per_m2\") == -1:\n    provider.addAttributes([QgsField(\"value_per_m2\", QVariant.Double, len=12, prec=2)])\n    layer.updateFields()\n\nprint(layer.fields().names())\n",[219,579,580,591,603,607,623,633,637,657,689,694,698],{"__ignoreMap":267},[271,581,582,584,586,588],{"class":176,"line":273},[271,583,277],{"class":276},[271,585,281],{"class":280},[271,587,284],{"class":276},[271,589,590],{"class":280}," QgsField, QgsProject\n",[271,592,593,595,598,600],{"class":176,"line":290},[271,594,277],{"class":276},[271,596,597],{"class":280}," qgis.PyQt.QtCore ",[271,599,284],{"class":276},[271,601,602],{"class":280}," QVariant\n",[271,604,605],{"class":176,"line":297},[271,606,294],{"emptyLinePlaceholder":293},[271,608,609,611,613,615,617,619,621],{"class":176,"line":322},[271,610,300],{"class":280},[271,612,303],{"class":276},[271,614,306],{"class":280},[271,616,310],{"class":309},[271,618,313],{"class":280},[271,620,46],{"class":316},[271,622,319],{"class":280},[271,624,625,628,630],{"class":176,"line":327},[271,626,627],{"class":280},"provider ",[271,629,303],{"class":276},[271,631,632],{"class":280}," layer.dataProvider()\n",[271,634,635],{"class":176,"line":342},[271,636,294],{"emptyLinePlaceholder":293},[271,638,639,641,643,646,649,651,653,655],{"class":176,"line":395},[271,640,521],{"class":276},[271,642,511],{"class":280},[271,644,645],{"class":309},"\"value_per_m2\"",[271,647,648],{"class":280},") ",[271,650,527],{"class":276},[271,652,530],{"class":276},[271,654,533],{"class":316},[271,656,536],{"class":280},[271,658,659,662,664,667,671,673,676,678,681,683,686],{"class":176,"line":400},[271,660,661],{"class":280},"    provider.addAttributes([QgsField(",[271,663,645],{"class":309},[271,665,666],{"class":280},", QVariant.Double, ",[271,668,670],{"class":669},"s9osk","len",[271,672,303],{"class":276},[271,674,675],{"class":316},"12",[271,677,422],{"class":280},[271,679,680],{"class":669},"prec",[271,682,303],{"class":276},[271,684,685],{"class":316},"2",[271,687,688],{"class":280},")])\n",[271,690,691],{"class":176,"line":411},[271,692,693],{"class":280},"    layer.updateFields()\n",[271,695,696],{"class":176,"line":437},[271,697,294],{"emptyLinePlaceholder":293},[271,699,700,702],{"class":176,"line":458},[271,701,414],{"class":316},[271,703,704],{"class":280},"(layer.fields().names())\n",[14,706,707,556,709,711,712,422,715,422,718,422,721,724,725,727,728,730,731,734,735,737,738,230],{},[211,708,481],{},[219,710,252],{}," takes a Qt type — ",[219,713,714],{},"QVariant.Double",[219,716,717],{},"QVariant.Int",[219,719,720],{},"QVariant.String",[219,722,723],{},"QVariant.Date",". ",[219,726,670],{}," and ",[219,729,680],{}," are honoured by providers that store them, and ignored by those that do not. ",[219,732,733],{},"updateFields()"," re-reads the schema into the layer object; without it, ",[219,736,485],{}," keeps returning the old list and every subsequent lookup by name fails. The existence check makes the script re-runnable, which matters in any ",[21,739,741],{"href":740},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002F","batch processing pipeline",[14,743,744],{},"Deleting is the mirror image, and this is where positional indexes bite: delete field 3 and every field after it moves down one. Resolve all the indexes first, then delete in descending order.",[262,746,748],{"className":264,"code":747,"language":266,"meta":267,"style":267},"targets = [\"temp_calc\", \"scratch\"]\nindexes = sorted(\n    (layer.fields().indexOf(name) for name in targets if layer.fields().indexOf(name) != -1),\n    reverse=True,\n)\nif indexes:\n    layer.dataProvider().deleteAttributes(indexes)\n    layer.updateFields()\n",[219,749,750,770,783,813,826,830,837,842],{"__ignoreMap":267},[271,751,752,755,757,760,763,765,768],{"class":176,"line":273},[271,753,754],{"class":280},"targets ",[271,756,303],{"class":276},[271,758,759],{"class":280}," [",[271,761,762],{"class":309},"\"temp_calc\"",[271,764,422],{"class":280},[271,766,767],{"class":309},"\"scratch\"",[271,769,319],{"class":280},[271,771,772,775,777,780],{"class":176,"line":290},[271,773,774],{"class":280},"indexes ",[271,776,303],{"class":276},[271,778,779],{"class":316}," sorted",[271,781,782],{"class":280},"(\n",[271,784,785,788,790,793,795,798,800,803,806,808,810],{"class":176,"line":297},[271,786,787],{"class":280},"    (layer.fields().indexOf(name) ",[271,789,330],{"class":276},[271,791,792],{"class":280}," name ",[271,794,336],{"class":276},[271,796,797],{"class":280}," targets ",[271,799,521],{"class":276},[271,801,802],{"class":280}," layer.fields().indexOf(name) ",[271,804,805],{"class":276},"!=",[271,807,530],{"class":276},[271,809,533],{"class":316},[271,811,812],{"class":280},"),\n",[271,814,815,818,820,823],{"class":176,"line":322},[271,816,817],{"class":669},"    reverse",[271,819,303],{"class":276},[271,821,822],{"class":316},"True",[271,824,825],{"class":280},",\n",[271,827,828],{"class":176,"line":327},[271,829,392],{"class":280},[271,831,832,834],{"class":176,"line":342},[271,833,521],{"class":276},[271,835,836],{"class":280}," indexes:\n",[271,838,839],{"class":176,"line":395},[271,840,841],{"class":280},"    layer.dataProvider().deleteAttributes(indexes)\n",[271,843,844],{"class":176,"line":400},[271,845,693],{"class":280},[14,847,848,850,851,855,856,858,859,863],{},[211,849,481],{}," Sorting descending means each deletion only affects indexes ",[852,853,854],"em",{},"after"," the one being removed, which have already been handled. Filtering out ",[219,857,563],{}," avoids passing a bogus index. ",[21,860,862],{"href":861},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fdelete-fields-pyqgis\u002F","Delete and Rename Fields in PyQGIS"," covers renaming, which has its own provider-support caveat.",[200,865,867],{"id":866},"updating-values-in-bulk","Updating values in bulk",[14,869,870],{},"The naive update — start editing, loop, set each value, commit — works but is slow, because every call goes through the undo stack. For anything above a few thousand features, batch the changes into a single dictionary and hand them to the provider in one call.",[262,872,874],{"className":264,"code":873,"language":266,"meta":267,"style":267},"from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nfield_index = layer.fields().indexOf(\"value_per_m2\")\n\nupdates = {}\nfor feature in layer.getFeatures():\n    area = feature.geometry().area()\n    value = feature[\"assessed_value\"]\n    if area and value is not None:\n        updates[feature.id()] = {field_index: round(value \u002F area, 2)}\n\nlayer.dataProvider().changeAttributeValues(updates)\nlayer.updateExtents()\n",[219,875,876,887,891,907,920,924,934,946,956,970,995,1022,1027,1033],{"__ignoreMap":267},[271,877,878,880,882,884],{"class":176,"line":273},[271,879,277],{"class":276},[271,881,281],{"class":280},[271,883,284],{"class":276},[271,885,886],{"class":280}," QgsProject\n",[271,888,889],{"class":176,"line":290},[271,890,294],{"emptyLinePlaceholder":293},[271,892,893,895,897,899,901,903,905],{"class":176,"line":297},[271,894,300],{"class":280},[271,896,303],{"class":276},[271,898,306],{"class":280},[271,900,310],{"class":309},[271,902,313],{"class":280},[271,904,46],{"class":316},[271,906,319],{"class":280},[271,908,909,912,914,916,918],{"class":176,"line":322},[271,910,911],{"class":280},"field_index ",[271,913,303],{"class":276},[271,915,511],{"class":280},[271,917,645],{"class":309},[271,919,392],{"class":280},[271,921,922],{"class":176,"line":327},[271,923,294],{"emptyLinePlaceholder":293},[271,925,926,929,931],{"class":176,"line":342},[271,927,928],{"class":280},"updates ",[271,930,303],{"class":276},[271,932,933],{"class":280}," {}\n",[271,935,936,938,941,943],{"class":176,"line":395},[271,937,330],{"class":276},[271,939,940],{"class":280}," feature ",[271,942,336],{"class":276},[271,944,945],{"class":280}," layer.getFeatures():\n",[271,947,948,951,953],{"class":176,"line":400},[271,949,950],{"class":280},"    area ",[271,952,303],{"class":276},[271,954,955],{"class":280}," feature.geometry().area()\n",[271,957,958,961,963,966,968],{"class":176,"line":411},[271,959,960],{"class":280},"    value ",[271,962,303],{"class":276},[271,964,965],{"class":280}," feature[",[271,967,514],{"class":309},[271,969,319],{"class":280},[271,971,972,975,978,981,984,987,990,993],{"class":176,"line":437},[271,973,974],{"class":276},"    if",[271,976,977],{"class":280}," area ",[271,979,980],{"class":276},"and",[271,982,983],{"class":280}," value ",[271,985,986],{"class":276},"is",[271,988,989],{"class":276}," not",[271,991,992],{"class":316}," None",[271,994,536],{"class":280},[271,996,997,1000,1002,1005,1008,1011,1014,1017,1019],{"class":176,"line":458},[271,998,999],{"class":280},"        updates[feature.id()] ",[271,1001,303],{"class":276},[271,1003,1004],{"class":280}," {field_index: ",[271,1006,1007],{"class":316},"round",[271,1009,1010],{"class":280},"(value ",[271,1012,1013],{"class":276},"\u002F",[271,1015,1016],{"class":280}," area, ",[271,1018,685],{"class":316},[271,1020,1021],{"class":280},")}\n",[271,1023,1025],{"class":176,"line":1024},12,[271,1026,294],{"emptyLinePlaceholder":293},[271,1028,1030],{"class":176,"line":1029},13,[271,1031,1032],{"class":280},"layer.dataProvider().changeAttributeValues(updates)\n",[271,1034,1036],{"class":176,"line":1035},14,[271,1037,1038],{"class":280},"layer.updateExtents()\n",[14,1040,1041,1043,1044,1047,1048,1051,1052,1055,1056,1059,1060,1062,1063,1066,1067,1069,1070,230],{},[211,1042,481],{}," The ",[219,1045,1046],{},"updates"," dictionary maps feature ID to a second dictionary of field index to new value, which is exactly the shape ",[219,1049,1050],{},"changeAttributeValues()"," expects. Building it first means one provider round-trip instead of ",[852,1053,1054],{},"n",". The ",[219,1057,1058],{},"if area and value is not None"," guard skips features with no geometry or a ",[219,1061,221],{}," value rather than writing garbage — ",[219,1064,1065],{},"None"," propagating into arithmetic is the attribute-side equivalent of the ",[219,1068,221],{}," behaviour described in ",[21,1071,1072],{"href":236},"Working with QGIS Expressions",[14,1074,1075,1076,1079],{},"When you ",[852,1077,1078],{},"do"," want undo support — in a plugin, where the user expects Ctrl+Z to work — use the edit buffer and make the commit path exception-safe:",[262,1081,1083],{"className":264,"code":1082,"language":266,"meta":267,"style":267},"layer.startEditing()\ntry:\n    for feature in layer.getFeatures():\n        layer.changeAttributeValue(feature.id(), field_index, 0)\n    if not layer.commitChanges():\n        raise RuntimeError(\"; \".join(layer.commitErrors()))\nexcept Exception:\n    layer.rollBack()\n    raise\n",[219,1084,1085,1090,1097,1108,1117,1126,1142,1152,1157],{"__ignoreMap":267},[271,1086,1087],{"class":176,"line":273},[271,1088,1089],{"class":280},"layer.startEditing()\n",[271,1091,1092,1095],{"class":176,"line":290},[271,1093,1094],{"class":276},"try",[271,1096,536],{"class":280},[271,1098,1099,1102,1104,1106],{"class":176,"line":297},[271,1100,1101],{"class":276},"    for",[271,1103,940],{"class":280},[271,1105,336],{"class":276},[271,1107,945],{"class":280},[271,1109,1110,1113,1115],{"class":176,"line":322},[271,1111,1112],{"class":280},"        layer.changeAttributeValue(feature.id(), field_index, ",[271,1114,46],{"class":316},[271,1116,392],{"class":280},[271,1118,1119,1121,1123],{"class":176,"line":327},[271,1120,974],{"class":276},[271,1122,989],{"class":276},[271,1124,1125],{"class":280}," layer.commitChanges():\n",[271,1127,1128,1131,1134,1136,1139],{"class":176,"line":342},[271,1129,1130],{"class":276},"        raise",[271,1132,1133],{"class":316}," RuntimeError",[271,1135,348],{"class":280},[271,1137,1138],{"class":309},"\"; \"",[271,1140,1141],{"class":280},".join(layer.commitErrors()))\n",[271,1143,1144,1147,1150],{"class":176,"line":395},[271,1145,1146],{"class":276},"except",[271,1148,1149],{"class":316}," Exception",[271,1151,536],{"class":280},[271,1153,1154],{"class":176,"line":400},[271,1155,1156],{"class":280},"    layer.rollBack()\n",[271,1158,1159],{"class":176,"line":411},[271,1160,1161],{"class":276},"    raise\n",[14,1163,1164,556,1166,1168,1169,1172,1173,1013,1175,1177,1178,1182],{},[211,1165,481],{},[219,1167,162],{}," returns a bool rather than raising, so ignoring its result is how partial writes go unnoticed; ",[219,1170,1171],{},"commitErrors()"," explains what the provider rejected. The ",[219,1174,1094],{},[219,1176,1146],{}," guarantees the layer never stays stuck in edit mode after a failure, which would otherwise block every later operation on it. ",[21,1179,1181],{"href":1180},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in Bulk with PyQGIS"," benchmarks the two approaches.",[200,1184,1186],{"id":1185},"calculating-a-field-with-an-expression","Calculating a field with an expression",[14,1188,1189,1190,1193],{},"Writing a Python loop to fill a column is fine, but when the logic is expressible as a QGIS expression there is a shorter and considerably faster route: ",[219,1191,1192],{},"native:fieldcalculator"," adds the field and computes it in a single Processing call, evaluated in C++ rather than per feature in Python.",[262,1195,1197],{"className":264,"code":1196,"language":266,"meta":267,"style":267},"import processing\n\nresult = processing.run(\"native:fieldcalculator\", {\n    \"INPUT\": \"\u002Fdata\u002Fparcels.gpkg|layername=parcels\",\n    \"FIELD_NAME\": \"value_per_m2\",\n    \"FIELD_TYPE\": 0,          # 0 = float, 1 = integer, 2 = string, 3 = date\n    \"FIELD_LENGTH\": 12,\n    \"FIELD_PRECISION\": 2,\n    \"FORMULA\": 'coalesce(\"assessed_value\", 0) \u002F nullif($area, 0)',\n    \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})[\"OUTPUT\"]\n",[219,1198,1199,1206,1210,1226,1239,1250,1266,1277,1288,1300,1312],{"__ignoreMap":267},[271,1200,1201,1203],{"class":176,"line":273},[271,1202,284],{"class":276},[271,1204,1205],{"class":280}," processing\n",[271,1207,1208],{"class":176,"line":290},[271,1209,294],{"emptyLinePlaceholder":293},[271,1211,1212,1215,1217,1220,1223],{"class":176,"line":297},[271,1213,1214],{"class":280},"result ",[271,1216,303],{"class":276},[271,1218,1219],{"class":280}," processing.run(",[271,1221,1222],{"class":309},"\"native:fieldcalculator\"",[271,1224,1225],{"class":280},", {\n",[271,1227,1228,1231,1234,1237],{"class":176,"line":322},[271,1229,1230],{"class":309},"    \"INPUT\"",[271,1232,1233],{"class":280},": ",[271,1235,1236],{"class":309},"\"\u002Fdata\u002Fparcels.gpkg|layername=parcels\"",[271,1238,825],{"class":280},[271,1240,1241,1244,1246,1248],{"class":176,"line":327},[271,1242,1243],{"class":309},"    \"FIELD_NAME\"",[271,1245,1233],{"class":280},[271,1247,645],{"class":309},[271,1249,825],{"class":280},[271,1251,1252,1255,1257,1259,1262],{"class":176,"line":342},[271,1253,1254],{"class":309},"    \"FIELD_TYPE\"",[271,1256,1233],{"class":280},[271,1258,46],{"class":316},[271,1260,1261],{"class":280},",          ",[271,1263,1265],{"class":1264},"sjoCn","# 0 = float, 1 = integer, 2 = string, 3 = date\n",[271,1267,1268,1271,1273,1275],{"class":176,"line":395},[271,1269,1270],{"class":309},"    \"FIELD_LENGTH\"",[271,1272,1233],{"class":280},[271,1274,675],{"class":316},[271,1276,825],{"class":280},[271,1278,1279,1282,1284,1286],{"class":176,"line":400},[271,1280,1281],{"class":309},"    \"FIELD_PRECISION\"",[271,1283,1233],{"class":280},[271,1285,685],{"class":316},[271,1287,825],{"class":280},[271,1289,1290,1293,1295,1298],{"class":176,"line":411},[271,1291,1292],{"class":309},"    \"FORMULA\"",[271,1294,1233],{"class":280},[271,1296,1297],{"class":309},"'coalesce(\"assessed_value\", 0) \u002F nullif($area, 0)'",[271,1299,825],{"class":280},[271,1301,1302,1305,1307,1310],{"class":176,"line":437},[271,1303,1304],{"class":309},"    \"OUTPUT\"",[271,1306,1233],{"class":280},[271,1308,1309],{"class":309},"\"TEMPORARY_OUTPUT\"",[271,1311,825],{"class":280},[271,1313,1314,1317,1320],{"class":176,"line":458},[271,1315,1316],{"class":280},"})[",[271,1318,1319],{"class":309},"\"OUTPUT\"",[271,1321,319],{"class":280},[14,1323,1324,556,1326,1329,1330,1333,1334,727,1337,1340,1341,1344,1345,1347,1348,1350,1351,230],{},[211,1325,481],{},[219,1327,1328],{},"FIELD_TYPE"," is an integer code, not a ",[219,1331,1332],{},"QVariant"," type — a frequent copy-paste error when moving between the two APIs. The formula is a QGIS expression, so ",[219,1335,1336],{},"$area",[219,1338,1339],{},"coalesce()"," are available; ",[219,1342,1343],{},"nullif($area, 0)"," converts a zero area into ",[219,1346,221],{}," so the division yields ",[219,1349,221],{}," instead of raising, which is the expression engine's idiom for a guarded divide. Because the algorithm writes a new layer rather than editing in place, it is safe to re-run and easy to slot into a ",[21,1352,1354],{"href":1353},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","chained Processing pipeline",[14,1356,1357],{},[29,1358,1361,1364,1367,1370,1373,1379,1385,1391,1394,1397,1402,1404,1409,1414,1419,1424],{"viewBox":1359,"role":32,"ariaLabel":1360,"xmlns":34},"0 0 760 240","Comparison of three ways to fill a calculated column: a Python loop with per-feature edits, a batched provider update, and the field calculator algorithm, ordered by speed",[36,1362,1363],{},"Three ways to fill a calculated column",[40,1365,1366],{},"Three horizontal bars compare approaches on a hundred thousand feature layer. A per-feature loop through the edit buffer is the slowest and is drawn as the longest bar. A single batched changeAttributeValues call is much shorter. The field calculator algorithm evaluated in C++ is the shortest. Each bar is annotated with what it is good for.",[44,1368],{"x":46,"y":46,"width":47,"height":1369,"fill":49},"240",[68,1371,1372],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Relative cost of filling one column on a large layer",[68,1374,1378],{"x":1375,"y":81,"style":1376,"fill":73,"textAnchor":1377},"200","text-anchor:end;font-size:12px;font-weight:bold;font-family:sans-serif","end","edit buffer, per feature",[44,1380],{"x":100,"y":101,"width":1381,"height":71,"rx":1382,"fill":166,"fillOpacity":1383,"stroke":166,"style":1384},"500","4",0.35,"stroke-width:2",[68,1386,1390],{"x":1387,"y":1388,"style":1389,"fill":66},"224","64","font-size:11.5px;font-family:sans-serif","every change hits the undo stack",[68,1392,1393],{"x":1375,"y":179,"style":1376,"fill":73,"textAnchor":1377},"batched provider update",[44,1395],{"x":100,"y":79,"width":1396,"height":71,"rx":1382,"fill":105,"fillOpacity":1383,"stroke":105,"style":1384},"188",[68,1398,1401],{"x":1399,"y":1400,"style":1389,"fill":66},"412","124","one round-trip, full Python control",[68,1403,1192],{"x":1375,"y":194,"style":1376,"fill":73,"textAnchor":1377},[44,1405],{"x":100,"y":1406,"width":1407,"height":71,"rx":1382,"fill":156,"fillOpacity":1408,"stroke":156,"style":1384},"166","96",0.4,[68,1410,1413],{"x":1411,"y":1412,"style":1389,"fill":66},"320","184","evaluated in C++, writes a new layer",[176,1415],{"x1":100,"y1":1416,"x2":1417,"y2":1416,"stroke":142,"style":1418},"206","712","stroke-width:1.5",[68,1420,1423],{"x":100,"y":1421,"style":1422,"fill":142},"226","font-size:11px;font-family:sans-serif","faster",[68,1425,1427],{"x":1417,"y":1421,"style":1426,"fill":142,"textAnchor":1377},"text-anchor:end;font-size:11px;font-family:sans-serif","slower",[14,1429,1430,1431,1433],{},"The trade-off is control. The field calculator cannot carry state between features, call an external library, or branch on anything the expression engine does not model — the same boundary described in ",[21,1432,1072],{"href":236},". When you cross it, fall back to the batched provider update above.",[200,1435,1437],{"id":1436},"aliases-constraints-and-default-values","Aliases, constraints and default values",[14,1439,1440],{},"A field is more than a name and a type. QGIS stores a layer of presentation and validation metadata on top of the schema, and setting it from a script is what turns a raw table into something a colleague can edit safely in the GUI.",[262,1442,1444],{"className":264,"code":1443,"language":266,"meta":267,"style":267},"from qgis.core import QgsDefaultValue, QgsFieldConstraints, QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nindex = layer.fields().indexOf(\"survey_date\")\n\nlayer.setFieldAlias(index, \"Date surveyed\")\nlayer.setDefaultValueDefinition(index, QgsDefaultValue(\"now()\", applyOnUpdate=False))\nlayer.setFieldConstraint(\n    index,\n    QgsFieldConstraints.ConstraintNotNull,\n    QgsFieldConstraints.ConstraintStrengthHard,\n)\n",[219,1445,1446,1457,1461,1477,1490,1494,1504,1525,1530,1535,1540,1545],{"__ignoreMap":267},[271,1447,1448,1450,1452,1454],{"class":176,"line":273},[271,1449,277],{"class":276},[271,1451,281],{"class":280},[271,1453,284],{"class":276},[271,1455,1456],{"class":280}," QgsDefaultValue, QgsFieldConstraints, QgsProject\n",[271,1458,1459],{"class":176,"line":290},[271,1460,294],{"emptyLinePlaceholder":293},[271,1462,1463,1465,1467,1469,1471,1473,1475],{"class":176,"line":297},[271,1464,300],{"class":280},[271,1466,303],{"class":276},[271,1468,306],{"class":280},[271,1470,310],{"class":309},[271,1472,313],{"class":280},[271,1474,46],{"class":316},[271,1476,319],{"class":280},[271,1478,1479,1481,1483,1485,1488],{"class":176,"line":322},[271,1480,506],{"class":280},[271,1482,303],{"class":276},[271,1484,511],{"class":280},[271,1486,1487],{"class":309},"\"survey_date\"",[271,1489,392],{"class":280},[271,1491,1492],{"class":176,"line":327},[271,1493,294],{"emptyLinePlaceholder":293},[271,1495,1496,1499,1502],{"class":176,"line":342},[271,1497,1498],{"class":280},"layer.setFieldAlias(index, ",[271,1500,1501],{"class":309},"\"Date surveyed\"",[271,1503,392],{"class":280},[271,1505,1506,1509,1512,1514,1517,1519,1522],{"class":176,"line":395},[271,1507,1508],{"class":280},"layer.setDefaultValueDefinition(index, QgsDefaultValue(",[271,1510,1511],{"class":309},"\"now()\"",[271,1513,422],{"class":280},[271,1515,1516],{"class":669},"applyOnUpdate",[271,1518,303],{"class":276},[271,1520,1521],{"class":316},"False",[271,1523,1524],{"class":280},"))\n",[271,1526,1527],{"class":176,"line":400},[271,1528,1529],{"class":280},"layer.setFieldConstraint(\n",[271,1531,1532],{"class":176,"line":411},[271,1533,1534],{"class":280},"    index,\n",[271,1536,1537],{"class":176,"line":437},[271,1538,1539],{"class":280},"    QgsFieldConstraints.ConstraintNotNull,\n",[271,1541,1542],{"class":176,"line":458},[271,1543,1544],{"class":280},"    QgsFieldConstraints.ConstraintStrengthHard,\n",[271,1546,1547],{"class":176,"line":1024},[271,1548,392],{"class":280},[14,1550,1551,556,1553,1556,1557,1560,1561,1564,1565,1568,1569,1572,1573,1576],{},[211,1552,481],{},[219,1554,1555],{},"setFieldAlias()"," changes only the display label, leaving the underlying name — and therefore every expression referencing it — untouched. ",[219,1558,1559],{},"QgsDefaultValue"," takes an ",[852,1562,1563],{},"expression",", not a literal, so ",[219,1566,1567],{},"now()"," is evaluated when a feature is created; ",[219,1570,1571],{},"applyOnUpdate=False"," means it stamps creation time rather than being rewritten on every edit. Constraints come in two strengths: a hard constraint blocks the commit, a soft one only warns. None of this is stored in the data source itself — it lives in the QGIS project or a companion ",[219,1574,1575],{},".qml",", so it travels with the project rather than the file.",[200,1578,1580],{"id":1579},"joining-attributes-between-layers","Joining attributes between layers",[14,1582,1583],{},"An attribute join attaches the columns of one layer to another where a key matches. QGIS offers two flavours and they behave very differently.",[14,1585,1586],{},[29,1587,1590,1593,1596,1599,1606,1609,1612,1617,1623,1627,1632,1636,1640,1643,1647,1649,1652,1655,1659,1662,1665,1669,1672,1676,1679,1682,1684,1686,1688,1692,1696,1699,1702,1704,1708,1711,1714],{"viewBox":1588,"role":32,"ariaLabel":1589,"xmlns":34},"0 0 760 272","Comparison of a virtual layer join, which is a live view added to the target layer, against a materialised join that writes a new permanent layer",[36,1591,1592],{},"Virtual join versus materialised join",[40,1594,1595],{},"On the left, addJoin attaches a CSV table to a parcels layer as a live view: the joined columns are read-only, prefixed, and disappear when the join is removed. On the right, the join field algorithm copies the matched values into a new output layer where the columns are real, writable and permanent.",[44,1597],{"x":46,"y":46,"width":47,"height":1598,"fill":49},"272",[51,1600,1601],{},[54,1602,1604],{"id":1603,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"attrJoinArrow",[63,1605],{"d":65,"fill":66},[68,1607,1608],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"A live view, or a real copy — pick by what happens next",[44,1610],{"x":78,"y":155,"width":1611,"height":100,"rx":82,"fill":104,"stroke":121,"style":85},"356",[68,1613,1616],{"x":1614,"y":1615,"style":90,"fill":121,"textAnchor":74},"194","68","layer.addJoin() — virtual",[44,1618],{"x":1619,"y":1620,"width":1621,"height":1407,"rx":1622,"fill":104,"stroke":142,"style":1418},"36","86","140","6",[68,1624,1626],{"x":79,"y":79,"style":1625,"fill":73,"textAnchor":74},"text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","parcels",[68,1628,1631],{"x":79,"y":1629,"style":1630,"fill":66,"textAnchor":74},"128","text-anchor:middle;font-size:11px;font-family:sans-serif","parcel_id",[68,1633,1635],{"x":79,"y":1634,"style":1630,"fill":66,"textAnchor":74},"148","zoning",[68,1637,1639],{"x":79,"y":1638,"style":1630,"fill":66,"textAnchor":74},"168","geometry",[44,1641],{"x":100,"y":1620,"width":1621,"height":1407,"rx":1622,"fill":104,"stroke":121,"style":1642},"stroke-width:1.5;stroke-dasharray:5 4",[68,1644,1646],{"x":1645,"y":79,"style":1625,"fill":121,"textAnchor":74},"282","valuations.csv",[68,1648,1631],{"x":1645,"y":1629,"style":1630,"fill":66,"textAnchor":74},[68,1650,1651],{"x":1645,"y":1634,"style":1630,"fill":66,"textAnchor":74},"assessed",[68,1653,1654],{"x":1645,"y":1638,"style":1630,"fill":66,"textAnchor":74},"year",[176,1656],{"x1":1657,"y1":1629,"x2":180,"y2":1629,"stroke":121,"style":1658},"176","stroke-width:2;marker-end:url(#attrJoinArrow)",[68,1660,1661],{"x":1614,"y":1416,"style":96,"fill":66,"textAnchor":74},"columns appear as val_assessed",[68,1663,1664],{"x":1614,"y":1421,"style":96,"fill":66,"textAnchor":74},"read-only · re-read on every draw",[68,1666,1668],{"x":1614,"y":1667,"style":96,"fill":105,"textAnchor":74},"246","breaks if the CSV moves",[44,1670],{"x":1671,"y":155,"width":1611,"height":100,"rx":82,"fill":104,"stroke":156,"style":85},"388",[68,1673,1675],{"x":1674,"y":1615,"style":90,"fill":156,"textAnchor":74},"566","native:joinattributestable — copied",[44,1677],{"x":1678,"y":1620,"width":1621,"height":1407,"rx":1622,"fill":104,"stroke":142,"style":1418},"408",[68,1680,1626],{"x":1681,"y":79,"style":1625,"fill":73,"textAnchor":74},"478",[68,1683,1631],{"x":1681,"y":1629,"style":1630,"fill":66,"textAnchor":74},[68,1685,1635],{"x":1681,"y":1634,"style":1630,"fill":66,"textAnchor":74},[68,1687,1639],{"x":1681,"y":1638,"style":1630,"fill":66,"textAnchor":74},[44,1689],{"x":1690,"y":1620,"width":1621,"height":1407,"rx":1622,"fill":156,"fillOpacity":1691,"stroke":156,"style":1384},"584",0.1,[68,1693,1695],{"x":1694,"y":79,"style":1625,"fill":156,"textAnchor":74},"654","parcels_valued",[68,1697,1698],{"x":1694,"y":1629,"style":1630,"fill":66,"textAnchor":74},"parcel_id · zoning",[68,1700,1701],{"x":1694,"y":1634,"style":1630,"fill":66,"textAnchor":74},"assessed · year",[68,1703,1639],{"x":1694,"y":1638,"style":1630,"fill":66,"textAnchor":74},[176,1705],{"x1":1706,"y1":1629,"x2":1707,"y2":1629,"stroke":156,"style":1658},"548","580",[68,1709,1710],{"x":1674,"y":1416,"style":96,"fill":66,"textAnchor":74},"real, writable columns",[68,1712,1713],{"x":1674,"y":1421,"style":96,"fill":66,"textAnchor":74},"a snapshot, not a live link",[68,1715,1716],{"x":1674,"y":1667,"style":96,"fill":156,"textAnchor":74},"safe to hand to someone else",[262,1718,1720],{"className":264,"code":1719,"language":266,"meta":267,"style":267},"import processing\n\njoined = processing.run(\"native:joinattributestable\", {\n    \"INPUT\": \"\u002Fdata\u002Fparcels.gpkg|layername=parcels\",\n    \"FIELD\": \"parcel_id\",\n    \"INPUT_2\": \"\u002Fdata\u002Fvaluations.csv\",\n    \"FIELD_2\": \"parcel_id\",\n    \"FIELDS_TO_COPY\": [\"assessed\", \"year\"],\n    \"METHOD\": 1,          # 1 = take attributes of the first matching feature only\n    \"DISCARD_NONMATCHING\": False,\n    \"OUTPUT\": \"\u002Fdata\u002Foutput\u002Fparcels_valued.gpkg\",\n})[\"OUTPUT\"]\n",[219,1721,1722,1728,1732,1746,1756,1768,1780,1791,1810,1824,1835,1846],{"__ignoreMap":267},[271,1723,1724,1726],{"class":176,"line":273},[271,1725,284],{"class":276},[271,1727,1205],{"class":280},[271,1729,1730],{"class":176,"line":290},[271,1731,294],{"emptyLinePlaceholder":293},[271,1733,1734,1737,1739,1741,1744],{"class":176,"line":297},[271,1735,1736],{"class":280},"joined ",[271,1738,303],{"class":276},[271,1740,1219],{"class":280},[271,1742,1743],{"class":309},"\"native:joinattributestable\"",[271,1745,1225],{"class":280},[271,1747,1748,1750,1752,1754],{"class":176,"line":322},[271,1749,1230],{"class":309},[271,1751,1233],{"class":280},[271,1753,1236],{"class":309},[271,1755,825],{"class":280},[271,1757,1758,1761,1763,1766],{"class":176,"line":327},[271,1759,1760],{"class":309},"    \"FIELD\"",[271,1762,1233],{"class":280},[271,1764,1765],{"class":309},"\"parcel_id\"",[271,1767,825],{"class":280},[271,1769,1770,1773,1775,1778],{"class":176,"line":342},[271,1771,1772],{"class":309},"    \"INPUT_2\"",[271,1774,1233],{"class":280},[271,1776,1777],{"class":309},"\"\u002Fdata\u002Fvaluations.csv\"",[271,1779,825],{"class":280},[271,1781,1782,1785,1787,1789],{"class":176,"line":395},[271,1783,1784],{"class":309},"    \"FIELD_2\"",[271,1786,1233],{"class":280},[271,1788,1765],{"class":309},[271,1790,825],{"class":280},[271,1792,1793,1796,1799,1802,1804,1807],{"class":176,"line":400},[271,1794,1795],{"class":309},"    \"FIELDS_TO_COPY\"",[271,1797,1798],{"class":280},": [",[271,1800,1801],{"class":309},"\"assessed\"",[271,1803,422],{"class":280},[271,1805,1806],{"class":309},"\"year\"",[271,1808,1809],{"class":280},"],\n",[271,1811,1812,1815,1817,1819,1821],{"class":176,"line":411},[271,1813,1814],{"class":309},"    \"METHOD\"",[271,1816,1233],{"class":280},[271,1818,533],{"class":316},[271,1820,1261],{"class":280},[271,1822,1823],{"class":1264},"# 1 = take attributes of the first matching feature only\n",[271,1825,1826,1829,1831,1833],{"class":176,"line":437},[271,1827,1828],{"class":309},"    \"DISCARD_NONMATCHING\"",[271,1830,1233],{"class":280},[271,1832,1521],{"class":316},[271,1834,825],{"class":280},[271,1836,1837,1839,1841,1844],{"class":176,"line":458},[271,1838,1304],{"class":309},[271,1840,1233],{"class":280},[271,1842,1843],{"class":309},"\"\u002Fdata\u002Foutput\u002Fparcels_valued.gpkg\"",[271,1845,825],{"class":280},[271,1847,1848,1850,1852],{"class":176,"line":1024},[271,1849,1316],{"class":280},[271,1851,1319],{"class":309},[271,1853,319],{"class":280},[14,1855,1856,556,1858,1861,1862,1864,1865,1867,1868,1871,1872,1874,1875,1877,1878,1880,1881,1885],{},[211,1857,481],{},[219,1859,1860],{},"METHOD"," decides what happens when the key matches more than one row — ",[219,1863,533],{}," keeps only the first match, ",[219,1866,46],{}," creates one output feature per match. ",[219,1869,1870],{},"DISCARD_NONMATCHING"," set to ",[219,1873,1521],{}," keeps unmatched parcels with ",[219,1876,221],{}," in the joined columns, which is usually what you want for auditing; flipping it to ",[219,1879,822],{}," silently drops them. Writing to a GeoPackage produces a self-contained result with no dependency on the CSV. ",[21,1882,1884],{"href":1883},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fjoin-attributes-by-field-value-pyqgis\u002F","Join Attributes by Field Value in PyQGIS"," covers key-type mismatches, which cause the great majority of \"the join returned all NULLs\" reports.",[200,1887,1889],{"id":1888},"summarising-by-attribute","Summarising by attribute",[14,1891,1892,1893,1896],{},"Counting features per category is the most common reporting task, and Python's ",[219,1894,1895],{},"collections.Counter"," combined with a targeted feature request handles it without loading geometry at all.",[262,1898,1900],{"className":264,"code":1899,"language":266,"meta":267,"style":267},"from collections import Counter\nfrom qgis.core import QgsFeatureRequest, QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nrequest = QgsFeatureRequest()\nrequest.setFlags(QgsFeatureRequest.NoGeometry)\nrequest.setSubsetOfAttributes([\"zoning\"], layer.fields())\n\ncounts = Counter(feature[\"zoning\"] for feature in layer.getFeatures(request))\nfor zoning, count in counts.most_common():\n    print(f\"{zoning or '(no value)':\u003C16} {count}\")\n",[219,1901,1902,1914,1925,1929,1945,1949,1959,1964,1975,1979,2003,2015],{"__ignoreMap":267},[271,1903,1904,1906,1909,1911],{"class":176,"line":273},[271,1905,277],{"class":276},[271,1907,1908],{"class":280}," collections ",[271,1910,284],{"class":276},[271,1912,1913],{"class":280}," Counter\n",[271,1915,1916,1918,1920,1922],{"class":176,"line":290},[271,1917,277],{"class":276},[271,1919,281],{"class":280},[271,1921,284],{"class":276},[271,1923,1924],{"class":280}," QgsFeatureRequest, QgsProject\n",[271,1926,1927],{"class":176,"line":297},[271,1928,294],{"emptyLinePlaceholder":293},[271,1930,1931,1933,1935,1937,1939,1941,1943],{"class":176,"line":322},[271,1932,300],{"class":280},[271,1934,303],{"class":276},[271,1936,306],{"class":280},[271,1938,310],{"class":309},[271,1940,313],{"class":280},[271,1942,46],{"class":316},[271,1944,319],{"class":280},[271,1946,1947],{"class":176,"line":327},[271,1948,294],{"emptyLinePlaceholder":293},[271,1950,1951,1954,1956],{"class":176,"line":342},[271,1952,1953],{"class":280},"request ",[271,1955,303],{"class":276},[271,1957,1958],{"class":280}," QgsFeatureRequest()\n",[271,1960,1961],{"class":176,"line":395},[271,1962,1963],{"class":280},"request.setFlags(QgsFeatureRequest.NoGeometry)\n",[271,1965,1966,1969,1972],{"class":176,"line":400},[271,1967,1968],{"class":280},"request.setSubsetOfAttributes([",[271,1970,1971],{"class":309},"\"zoning\"",[271,1973,1974],{"class":280},"], layer.fields())\n",[271,1976,1977],{"class":176,"line":411},[271,1978,294],{"emptyLinePlaceholder":293},[271,1980,1981,1984,1986,1989,1991,1994,1996,1998,2000],{"class":176,"line":437},[271,1982,1983],{"class":280},"counts ",[271,1985,303],{"class":276},[271,1987,1988],{"class":280}," Counter(feature[",[271,1990,1971],{"class":309},[271,1992,1993],{"class":280},"] ",[271,1995,330],{"class":276},[271,1997,940],{"class":280},[271,1999,336],{"class":276},[271,2001,2002],{"class":280}," layer.getFeatures(request))\n",[271,2004,2005,2007,2010,2012],{"class":176,"line":458},[271,2006,330],{"class":276},[271,2008,2009],{"class":280}," zoning, count ",[271,2011,336],{"class":276},[271,2013,2014],{"class":280}," counts.most_common():\n",[271,2016,2017,2019,2021,2023,2025,2027,2030,2033,2036,2039,2041,2043,2046,2048,2050],{"class":176,"line":1024},[271,2018,345],{"class":316},[271,2020,348],{"class":280},[271,2022,351],{"class":276},[271,2024,354],{"class":309},[271,2026,357],{"class":316},[271,2028,2029],{"class":280},"zoning ",[271,2031,2032],{"class":276},"or",[271,2034,2035],{"class":309}," '(no value)'",[271,2037,2038],{"class":276},":\u003C16",[271,2040,366],{"class":316},[271,2042,369],{"class":316},[271,2044,2045],{"class":280},"count",[271,2047,366],{"class":316},[271,2049,354],{"class":309},[271,2051,392],{"class":280},[14,2053,2054,556,2056,2059,2060,2063,2064,2067,2068,2070,2071,724,2073,2077,2078,2081],{},[211,2055,481],{},[219,2057,2058],{},"NoGeometry"," tells the provider not to read or decode geometry, which on a polygon layer is by far the biggest cost of iteration. ",[219,2061,2062],{},"setSubsetOfAttributes()"," narrows it further to the one column being counted. Together they turn a full-table read into something closer to a column scan. The ",[219,2065,2066],{},"or '(no value)'"," renders ",[219,2069,221],{}," readably instead of printing ",[219,2072,1065],{},[21,2074,2076],{"href":2075},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fcount-features-by-attribute-pyqgis\u002F","Count and Summarise Features by Attribute in PyQGIS"," extends this to sums and means, and to the ",[219,2079,2080],{},"native:statisticsbycategories"," algorithm when the result needs to be a layer.",[200,2083,2085],{"id":2084},"exporting-the-attribute-table","Exporting the attribute table",[14,2087,2088,2089,2092],{},"Handing a table to someone who does not use QGIS means CSV, and ",[219,2090,2091],{},"QgsVectorFileWriter"," produces it with correct quoting and encoding — considerably safer than assembling the lines yourself.",[262,2094,2096],{"className":264,"code":2095,"language":266,"meta":267,"style":267},"from qgis.core import QgsCoordinateTransformContext, QgsProject, QgsVectorFileWriter\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\noptions = QgsVectorFileWriter.SaveVectorOptions()\noptions.driverName = \"CSV\"\noptions.fileEncoding = \"UTF-8\"\noptions.attributes = [layer.fields().indexOf(n) for n in (\"parcel_id\", \"zoning\", \"assessed_value\")]\noptions.layerOptions = [\"GEOMETRY=AS_WKT\"]\n\nerror, message, _, _ = QgsVectorFileWriter.writeAsVectorFormatV3(\n    layer, \"\u002Fdata\u002Foutput\u002Fparcels.csv\", QgsCoordinateTransformContext(), options\n)\nif error != QgsVectorFileWriter.NoError:\n    raise RuntimeError(message)\n",[219,2097,2098,2109,2113,2129,2133,2143,2153,2163,2196,2210,2214,2224,2235,2239,2251],{"__ignoreMap":267},[271,2099,2100,2102,2104,2106],{"class":176,"line":273},[271,2101,277],{"class":276},[271,2103,281],{"class":280},[271,2105,284],{"class":276},[271,2107,2108],{"class":280}," QgsCoordinateTransformContext, QgsProject, QgsVectorFileWriter\n",[271,2110,2111],{"class":176,"line":290},[271,2112,294],{"emptyLinePlaceholder":293},[271,2114,2115,2117,2119,2121,2123,2125,2127],{"class":176,"line":297},[271,2116,300],{"class":280},[271,2118,303],{"class":276},[271,2120,306],{"class":280},[271,2122,310],{"class":309},[271,2124,313],{"class":280},[271,2126,46],{"class":316},[271,2128,319],{"class":280},[271,2130,2131],{"class":176,"line":322},[271,2132,294],{"emptyLinePlaceholder":293},[271,2134,2135,2138,2140],{"class":176,"line":327},[271,2136,2137],{"class":280},"options ",[271,2139,303],{"class":276},[271,2141,2142],{"class":280}," QgsVectorFileWriter.SaveVectorOptions()\n",[271,2144,2145,2148,2150],{"class":176,"line":342},[271,2146,2147],{"class":280},"options.driverName ",[271,2149,303],{"class":276},[271,2151,2152],{"class":309}," \"CSV\"\n",[271,2154,2155,2158,2160],{"class":176,"line":395},[271,2156,2157],{"class":280},"options.fileEncoding ",[271,2159,303],{"class":276},[271,2161,2162],{"class":309}," \"UTF-8\"\n",[271,2164,2165,2168,2170,2173,2175,2178,2180,2183,2185,2187,2189,2191,2193],{"class":176,"line":400},[271,2166,2167],{"class":280},"options.attributes ",[271,2169,303],{"class":276},[271,2171,2172],{"class":280}," [layer.fields().indexOf(n) ",[271,2174,330],{"class":276},[271,2176,2177],{"class":280}," n ",[271,2179,336],{"class":276},[271,2181,2182],{"class":280}," (",[271,2184,1765],{"class":309},[271,2186,422],{"class":280},[271,2188,1971],{"class":309},[271,2190,422],{"class":280},[271,2192,514],{"class":309},[271,2194,2195],{"class":280},")]\n",[271,2197,2198,2201,2203,2205,2208],{"class":176,"line":411},[271,2199,2200],{"class":280},"options.layerOptions ",[271,2202,303],{"class":276},[271,2204,759],{"class":280},[271,2206,2207],{"class":309},"\"GEOMETRY=AS_WKT\"",[271,2209,319],{"class":280},[271,2211,2212],{"class":176,"line":437},[271,2213,294],{"emptyLinePlaceholder":293},[271,2215,2216,2219,2221],{"class":176,"line":458},[271,2217,2218],{"class":280},"error, message, _, _ ",[271,2220,303],{"class":276},[271,2222,2223],{"class":280}," QgsVectorFileWriter.writeAsVectorFormatV3(\n",[271,2225,2226,2229,2232],{"class":176,"line":1024},[271,2227,2228],{"class":280},"    layer, ",[271,2230,2231],{"class":309},"\"\u002Fdata\u002Foutput\u002Fparcels.csv\"",[271,2233,2234],{"class":280},", QgsCoordinateTransformContext(), options\n",[271,2236,2237],{"class":176,"line":1029},[271,2238,392],{"class":280},[271,2240,2241,2243,2246,2248],{"class":176,"line":1035},[271,2242,521],{"class":276},[271,2244,2245],{"class":280}," error ",[271,2247,805],{"class":276},[271,2249,2250],{"class":280}," QgsVectorFileWriter.NoError:\n",[271,2252,2254,2256,2258],{"class":176,"line":2253},15,[271,2255,541],{"class":276},[271,2257,1133],{"class":316},[271,2259,2260],{"class":280},"(message)\n",[14,2262,2263,556,2265,2268,2269,2272,2273,2276,2277,2280,2281,2285],{},[211,2264,481],{},[219,2266,2267],{},"SaveVectorOptions"," collects the export settings; ",[219,2270,2271],{},"attributes"," restricts the columns by index, and omitting it exports everything. ",[219,2274,2275],{},"GEOMETRY=AS_WKT"," adds a WKT column — drop it for a pure attribute export. ",[219,2278,2279],{},"writeAsVectorFormatV3()"," returns a tuple whose first element is an error code, and checking it is essential because the function does not raise on failure. See ",[21,2282,2284],{"href":2283},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fexport-attribute-table-to-csv-pyqgis\u002F","Export an Attribute Table to CSV with PyQGIS"," for encoding pitfalls and the delimiter options.",[200,2287,2289],{"id":2288},"handling-null-empty-and-missing","Handling NULL, empty and missing",[14,2291,2292],{},"Three different absences look alike in a printed table and behave completely differently in code, and conflating them is the source of a surprising share of attribute bugs.",[14,2294,2295,2300,2301,2303,2304,2307,2308,2310],{},[211,2296,2297,2299],{},[219,2298,221],{}," means \"no value recorded.\""," PyQGIS surfaces it as Python ",[219,2302,1065],{}," when reading through ",[219,2305,2306],{},"feature[\"field\"]",". Arithmetic involving it yields ",[219,2309,1065],{},", comparisons against it are false, and it is the only one of the three that survives a round-trip through GeoPackage and PostGIS faithfully.",[14,2312,2313,2316,2317,2320,2321,2323],{},[211,2314,2315],{},"An empty string is a value"," — the zero-length text ",[219,2318,2319],{},"\"\"",". A shapefile cannot distinguish it from ",[219,2322,221],{},", which is why a dataset that round-trips through shapefile comes back with one where it had the other.",[14,2325,2326,2329,2330,2333,2334,2337,2338,2341,2342,230],{},[211,2327,2328],{},"A missing field is a schema problem",", not a data problem. ",[219,2331,2332],{},"feature[\"nonexistent\"]"," raises ",[219,2335,2336],{},"KeyError",", while ",[219,2339,2340],{},"layer.fields().indexOf(\"nonexistent\")"," quietly returns ",[219,2343,563],{},[262,2345,2347],{"className":264,"code":2346,"language":266,"meta":267,"style":267},"from qgis.core import QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\ndef read(feature, name, default=None):\n    \"\"\"Read a field defensively: missing field and NULL both collapse to a default.\"\"\"\n    if layer.fields().indexOf(name) == -1:\n        return default\n    value = feature[name]\n    return default if value is None else value\n\nfor feature in layer.getFeatures():\n    owner = read(feature, \"owner_name\", \"\")\n    value = read(feature, \"assessed_value\", 0)\n    if not owner.strip():\n        print(f\"parcel {feature['parcel_id']} has no owner recorded\")\n",[219,2348,2349,2359,2363,2379,2383,2402,2407,2421,2429,2438,2460,2464,2474,2493,2509,2518],{"__ignoreMap":267},[271,2350,2351,2353,2355,2357],{"class":176,"line":273},[271,2352,277],{"class":276},[271,2354,281],{"class":280},[271,2356,284],{"class":276},[271,2358,886],{"class":280},[271,2360,2361],{"class":176,"line":290},[271,2362,294],{"emptyLinePlaceholder":293},[271,2364,2365,2367,2369,2371,2373,2375,2377],{"class":176,"line":297},[271,2366,300],{"class":280},[271,2368,303],{"class":276},[271,2370,306],{"class":280},[271,2372,310],{"class":309},[271,2374,313],{"class":280},[271,2376,46],{"class":316},[271,2378,319],{"class":280},[271,2380,2381],{"class":176,"line":322},[271,2382,294],{"emptyLinePlaceholder":293},[271,2384,2385,2388,2392,2395,2397,2399],{"class":176,"line":327},[271,2386,2387],{"class":276},"def",[271,2389,2391],{"class":2390},"svObZ"," read",[271,2393,2394],{"class":280},"(feature, name, default",[271,2396,303],{"class":276},[271,2398,1065],{"class":316},[271,2400,2401],{"class":280},"):\n",[271,2403,2404],{"class":176,"line":342},[271,2405,2406],{"class":309},"    \"\"\"Read a field defensively: missing field and NULL both collapse to a default.\"\"\"\n",[271,2408,2409,2411,2413,2415,2417,2419],{"class":176,"line":395},[271,2410,974],{"class":276},[271,2412,802],{"class":280},[271,2414,527],{"class":276},[271,2416,530],{"class":276},[271,2418,533],{"class":316},[271,2420,536],{"class":280},[271,2422,2423,2426],{"class":176,"line":400},[271,2424,2425],{"class":276},"        return",[271,2427,2428],{"class":280}," default\n",[271,2430,2431,2433,2435],{"class":176,"line":411},[271,2432,960],{"class":280},[271,2434,303],{"class":276},[271,2436,2437],{"class":280}," feature[name]\n",[271,2439,2440,2443,2446,2448,2450,2452,2454,2457],{"class":176,"line":437},[271,2441,2442],{"class":276},"    return",[271,2444,2445],{"class":280}," default ",[271,2447,521],{"class":276},[271,2449,983],{"class":280},[271,2451,986],{"class":276},[271,2453,992],{"class":316},[271,2455,2456],{"class":276}," else",[271,2458,2459],{"class":280}," value\n",[271,2461,2462],{"class":176,"line":458},[271,2463,294],{"emptyLinePlaceholder":293},[271,2465,2466,2468,2470,2472],{"class":176,"line":1024},[271,2467,330],{"class":276},[271,2469,940],{"class":280},[271,2471,336],{"class":276},[271,2473,945],{"class":280},[271,2475,2476,2479,2481,2484,2487,2489,2491],{"class":176,"line":1029},[271,2477,2478],{"class":280},"    owner ",[271,2480,303],{"class":276},[271,2482,2483],{"class":280}," read(feature, ",[271,2485,2486],{"class":309},"\"owner_name\"",[271,2488,422],{"class":280},[271,2490,2319],{"class":309},[271,2492,392],{"class":280},[271,2494,2495,2497,2499,2501,2503,2505,2507],{"class":176,"line":1035},[271,2496,960],{"class":280},[271,2498,303],{"class":276},[271,2500,2483],{"class":280},[271,2502,514],{"class":309},[271,2504,422],{"class":280},[271,2506,46],{"class":316},[271,2508,392],{"class":280},[271,2510,2511,2513,2515],{"class":176,"line":2253},[271,2512,974],{"class":276},[271,2514,989],{"class":276},[271,2516,2517],{"class":280}," owner.strip():\n",[271,2519,2521,2524,2526,2528,2531,2533,2536,2539,2542,2544,2547],{"class":176,"line":2520},16,[271,2522,2523],{"class":316},"        print",[271,2525,348],{"class":280},[271,2527,351],{"class":276},[271,2529,2530],{"class":309},"\"parcel ",[271,2532,357],{"class":316},[271,2534,2535],{"class":280},"feature[",[271,2537,2538],{"class":309},"'parcel_id'",[271,2540,2541],{"class":280},"]",[271,2543,366],{"class":316},[271,2545,2546],{"class":309}," has no owner recorded\"",[271,2548,392],{"class":280},[14,2550,2551,2553,2554,2557,2558,2560,2561,2563],{},[211,2552,481],{}," The helper separates the two failure modes but lets the caller treat them uniformly, which is usually what reporting code wants. Testing ",[219,2555,2556],{},"not owner.strip()"," catches ",[219,2559,221],{},", the empty string, and a cell containing only whitespace in one expression — the third case being what you get from a hand-maintained spreadsheet. When writing values back, prefer an explicit ",[219,2562,1065],{}," over an empty string so the distinction survives into GeoPackage.",[14,2565,2566,2567,2569,2570,2573,2574,2577,2578,2581,2582,2585],{},"A related gotcha: comparing to ",[219,2568,221],{}," in a filter expression needs ",[219,2571,2572],{},"IS NULL",", not ",[219,2575,2576],{},"= NULL",". The latter is valid syntax that is never true, so a ",[219,2579,2580],{},"QgsFeatureRequest"," filtered on ",[219,2583,2584],{},"\"owner_name\" = NULL"," returns nothing at all rather than the rows you meant.",[200,2587,2589],{"id":2588},"key-takeaways","Key takeaways",[205,2591,2592,2603,2612,2618,2630,2644,2650],{},[208,2593,2594,2597,2598,2600,2601,230],{},[211,2595,2596],{},"Know which route you are on."," Provider calls write immediately and cannot be undone; the edit buffer stages changes and must reach either ",[219,2599,162],{}," or ",[219,2602,170],{},[208,2604,2605,2611],{},[211,2606,2607,2608,2610],{},"Call ",[219,2609,733],{}," after a schema change."," Without it the layer keeps serving a stale field list and lookups by name fail.",[208,2613,2614,2617],{},[211,2615,2616],{},"Delete fields in descending index order."," Indexes are positional and shift as soon as one is removed.",[208,2619,2620,2623,2624,2626,2627,2629],{},[211,2621,2622],{},"Batch bulk updates."," One ",[219,2625,1050],{}," call with a full dictionary beats ",[852,2628,1054],{}," individual edits, often by an order of magnitude.",[208,2631,2632,2637,2638,2640,2641,2643],{},[211,2633,2634,2635,230],{},"Check ",[219,2636,162],{}," It returns ",[219,2639,1521],{}," rather than raising, and ",[219,2642,1171],{}," is the only place the reason appears.",[208,2645,2646,2649],{},[211,2647,2648],{},"Prefer a materialised join for anything you hand on."," A virtual join is a live link that breaks when the source file moves.",[208,2651,2652,556,2655,2658],{},[211,2653,2654],{},"Skip geometry when you only need attributes.",[219,2656,2657],{},"QgsFeatureRequest.NoGeometry"," plus a column subset is the cheapest way to read a table.",[200,2660,2662],{"id":2661},"frequently-asked-questions","Frequently Asked Questions",[14,2664,2665,2668,2669,2672,2673,2676],{},[211,2666,2667],{},"Why does my new field not appear after addAttributes()?","\nThe provider has it, but the layer object is still serving a cached schema. Call ",[219,2670,2671],{},"layer.updateFields()"," immediately after ",[219,2674,2675],{},"addAttributes()"," and the field will resolve by name from then on.",[14,2678,2679,2682,2683,2686,2687,230],{},[211,2680,2681],{},"Should I use the data provider or startEditing() to change values?","\nUse the provider for scripted bulk work where speed matters and undo does not. Use ",[219,2684,2685],{},"startEditing()"," inside a plugin, where the user expects the change to appear on the undo stack and to be discardable. Never leave a layer in edit mode after an exception — wrap the block and call ",[219,2688,170],{},[14,2690,2691,2694,2695,2697],{},[211,2692,2693],{},"Why did my attribute join produce only NULL values?","\nAlmost always a key-type mismatch: the parcels layer stores ",[219,2696,1631],{}," as an integer while the CSV loads it as text, so nothing matches. Cast one side to the other's type before joining, or import the CSV with an explicit field-type definition.",[14,2699,2700,2703,2704,2706,2707,2709,2710,2712,2713,2715],{},[211,2701,2702],{},"How do I count features per category without loading geometry?","\nBuild a ",[219,2705,2580],{},", set the ",[219,2708,2058],{}," flag, restrict it with ",[219,2711,2062],{}," to the category column, then feed the iterator to ",[219,2714,1895],{},". Geometry decoding is usually the dominant cost, so skipping it is the single biggest saving.",[14,2717,2718,2721,2722,2724],{},[211,2719,2720],{},"Is it safe to write attributes to a shapefile?","\nIt works, but the format will surprise you: field names are truncated to ten characters, there is no true date type, and ",[219,2723,221],{}," support is inconsistent. Write to GeoPackage instead unless a downstream consumer specifically requires a shapefile.",[200,2726,2728],{"id":2727},"related","Related",[205,2730,2731,2737,2743,2748,2753,2759,2763,2767,2771,2775],{},[208,2732,2733,2736],{},[21,2734,2735],{"href":23},"Spatial Data Processing & Automation with PyQGIS"," — the guide this page belongs to",[208,2738,2739],{},[21,2740,2742],{"href":2741},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002F","Vector Data Manipulation in PyQGIS",[208,2744,2745],{},[21,2746,2747],{"href":740},"Batch Processing with PyQGIS",[208,2749,2750],{},[21,2751,2752],{"href":236},"Working with QGIS Expressions in PyQGIS",[208,2754,2755],{},[21,2756,2758],{"href":2757},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fadd-field-to-layer-pyqgis\u002F","Add a Field to a Layer in PyQGIS",[208,2760,2761],{},[21,2762,1181],{"href":1180},[208,2764,2765],{},[21,2766,862],{"href":861},[208,2768,2769],{},[21,2770,1884],{"href":1883},[208,2772,2773],{},[21,2774,2284],{"href":2283},[208,2776,2777],{},[21,2778,2076],{"href":2075},[2780,2781,2782],"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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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 .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":267,"searchDepth":290,"depth":290,"links":2784},[2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797],{"id":202,"depth":290,"text":203},{"id":241,"depth":290,"text":242},{"id":570,"depth":290,"text":571},{"id":866,"depth":290,"text":867},{"id":1185,"depth":290,"text":1186},{"id":1436,"depth":290,"text":1437},{"id":1579,"depth":290,"text":1580},{"id":1888,"depth":290,"text":1889},{"id":2084,"depth":290,"text":2085},{"id":2288,"depth":290,"text":2289},{"id":2588,"depth":290,"text":2589},{"id":2661,"depth":290,"text":2662},{"id":2727,"depth":290,"text":2728},"Manage attribute tables in PyQGIS. Add and remove fields, update values in bulk, join layers by attribute, summarise by category, and export the table safely.","md",{"slug":2801,"type":2802,"breadcrumb":2803,"datePublished":2804,"dateModified":2804},"attribute-tables-and-field-management","guide","Attribute Tables & Fields","2026-08-01","\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management",{"title":5,"description":2798},"spatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Findex","T8OSKfZKb3ea7I1d0Xdftrl3mfIC0TnFBwrOensNf38",1785585990475]