[{"data":1,"prerenderedAt":1296},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis":3},{"id":4,"title":5,"body":6,"description":1285,"extension":1286,"meta":1287,"navigation":277,"path":1292,"seo":1293,"stem":1294,"__hash__":1295},"docs\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis\u002Findex.md","Edit Features with Transactions in PyQGIS",{"type":7,"value":8,"toc":1271},"minimark",[9,13,30,39,215,220,241,245,452,486,489,493,561,566,569,634,643,647,719,722,725,791,807,811,817,932,943,954,958,961,981,989,992,996,999,1070,1084,1105,1109,1131,1135,1179,1183,1186,1190,1196,1209,1222,1232,1236,1267],[10,11,5],"h1",{"id":12},"edit-features-with-transactions-in-pyqgis",[14,15,16,17,21,22,25,26,29],"p",{},"There are two ways to change a feature in PyQGIS and they behave completely differently. The edit buffer — ",[18,19,20],"code",{},"startEditing",", change, ",[18,23,24],{},"commitChanges"," — stages everything in memory, participates in undo, and can be rolled back. The data provider — ",[18,27,28],{},"dataProvider().changeAttributeValues()"," — writes immediately, bypasses undo, and cannot be undone. Choosing the wrong one is how a script leaves a layer half-modified with no way back.",[14,31,32,33,38],{},"This recipe belongs to ",[34,35,37],"a",{"href":36},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002F","Vector Data Manipulation",". It covers both paths, when each is right, rollback, performance, and transaction groups that span several database layers.",[14,40,41],{},[42,43,48,52,56,63,80,89,99,105,115,121,128,132,136,140,143,148,153,159,163,167,172,176,180,183,187,190,194,198,201,205,211],"svg",{"viewBox":44,"role":45,"ariaLabel":46,"xmlns":47},"0 0 760 320","img","Two write paths compared: the edit buffer which stages changes in memory and can be committed or rolled back, and the provider which writes straight to the data source","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[49,50,51],"title",{},"Staged, or straight through",[53,54,55],"desc",{},"Edits made through the layer go into an in-memory buffer, take part in the undo stack, and reach the data source only on commit; a rollback discards them. Edits made through the data provider go straight to the data source with no buffer, no undo and no way back.",[57,58],"rect",{"x":59,"y":59,"width":60,"height":61,"fill":62},"0","760","320","#f6f3ea",[64,65,66],"defs",{},[67,68,75],"marker",{"id":69,"viewBox":70,"refX":71,"refY":72,"markerWidth":73,"markerHeight":73,"orient":74},"txArrow","0 0 10 10","8","5","7","auto-start-reverse",[76,77],"path",{"d":78,"fill":79},"M0 0 L10 5 L0 10 z","#2f3b35",[81,82,88],"text",{"x":83,"y":84,"style":85,"fill":86,"textAnchor":87},"380","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","One of these has a way back",[57,90],{"x":91,"y":92,"width":93,"height":94,"rx":95,"fill":96,"stroke":97,"style":98},"24","46","344","204","10","#eef7f4","#0f766e","stroke-width:2.5",[81,100,104],{"x":101,"y":102,"style":103,"fill":97,"textAnchor":87},"196","72","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","layer.startEditing()",[57,106],{"x":107,"y":108,"width":109,"height":110,"rx":111,"fill":112,"stroke":113,"style":114},"52","92","112","34","6","#e8efe6","#15803d","stroke-width:1.8",[81,116,120],{"x":117,"y":118,"style":119,"fill":79,"textAnchor":87},"108","114","text-anchor:middle;font-size:10px;font-family:sans-serif","your changes",[122,123],"line",{"x1":124,"y1":125,"x2":126,"y2":125,"stroke":79,"style":127},"164","109","190","stroke-width:1.8;marker-end:url(#txArrow)",[57,129],{"x":130,"y":108,"width":131,"height":110,"rx":111,"fill":112,"stroke":113,"style":114},"198","142",[81,133,135],{"x":134,"y":118,"style":119,"fill":79,"textAnchor":87},"269","edit buffer + undo",[76,137],{"d":138,"fill":139,"stroke":79,"style":127},"M269 126 L269 152 L140 152 L140 176","none",[76,141],{"d":142,"fill":139,"stroke":79,"style":127},"M269 126 L269 176",[57,144],{"x":107,"y":145,"width":146,"height":110,"rx":111,"fill":147,"stroke":113,"style":114},"180","176","#edf8e9",[81,149,152],{"x":150,"y":151,"style":119,"fill":79,"textAnchor":87},"140","202","commitChanges()",[57,154],{"x":155,"y":145,"width":156,"height":110,"rx":111,"fill":157,"stroke":158,"style":114},"238","102","#fdf2e2","#b45309",[81,160,162],{"x":161,"y":151,"style":119,"fill":79,"textAnchor":87},"289","rollBack()",[81,164,166],{"x":101,"y":155,"style":119,"fill":165,"textAnchor":87},"#59645f","nothing on disk until you commit",[57,168],{"x":169,"y":92,"width":93,"height":94,"rx":95,"fill":170,"stroke":171,"style":98},"392","#fffdf7","#b91c1c",[81,173,175],{"x":174,"y":102,"style":103,"fill":171,"textAnchor":87},"564","layer.dataProvider()",[57,177],{"x":178,"y":108,"width":109,"height":110,"rx":111,"fill":179,"stroke":158,"style":114},"420","#f3e9d8",[81,181,120],{"x":182,"y":118,"style":119,"fill":79,"textAnchor":87},"476",[122,184],{"x1":185,"y1":125,"x2":186,"y2":125,"stroke":79,"style":127},"532","558",[57,188],{"x":189,"y":108,"width":131,"height":110,"rx":111,"fill":179,"stroke":158,"style":114},"566",[81,191,193],{"x":192,"y":118,"style":119,"fill":79,"textAnchor":87},"637","the file, immediately",[81,195,197],{"x":174,"y":124,"style":196,"fill":79,"textAnchor":87},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","no buffer · no undo · no rollback",[81,199,200],{"x":174,"y":101,"style":196,"fill":79,"textAnchor":87},"faster on bulk writes",[81,202,204],{"x":174,"y":203,"style":119,"fill":171,"textAnchor":87},"228","an interrupted loop leaves half the changes",[57,206],{"x":207,"y":208,"width":209,"height":110,"rx":71,"fill":157,"stroke":158,"style":210},"120","272","520","stroke-width:2",[81,212,214],{"x":83,"y":213,"style":196,"fill":79,"textAnchor":87},"294","buffer for anything a person will see; provider for bulk work on a copy",[216,217,219],"h2",{"id":218},"prerequisites","Prerequisites",[221,222,223,231],"ul",{},[224,225,226,230],"li",{},[227,228,229],"strong",{},"QGIS 3.34 LTR"," or newer.",[224,232,233,234,237,238,240],{},"An editable layer — a GeoPackage, PostGIS table or shapefile. A layer opened read-only reports ",[18,235,236],{},"isEditable()"," false and refuses ",[18,239,20],{},".",[216,242,244],{"id":243},"the-edit-buffer","The edit buffer",[246,247,252],"pre",{"className":248,"code":249,"language":250,"meta":251,"style":251},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nindex = parcels.fields().indexOf(\"status\")\n\nif not parcels.startEditing():\n    raise SystemExit(\"layer is not editable\")\n\ntry:\n    for feature in parcels.getFeatures('\"area_m2\" > 5000'):\n        parcels.changeAttributeValue(feature.id(), index, \"large\")\n    if not parcels.commitChanges():\n        raise RuntimeError(\"; \".join(parcels.commitErrors()))\nexcept Exception:\n    parcels.rollBack()\n    raise\n","python","",[18,253,254,272,279,304,321,326,338,355,360,369,390,401,412,429,440,446],{"__ignoreMap":251},[255,256,258,262,266,269],"span",{"class":122,"line":257},1,[255,259,261],{"class":260},"snl16","from",[255,263,265],{"class":264},"s95oV"," qgis.core ",[255,267,268],{"class":260},"import",[255,270,271],{"class":264}," QgsProject\n",[255,273,275],{"class":122,"line":274},2,[255,276,278],{"emptyLinePlaceholder":277},true,"\n",[255,280,282,285,288,291,295,298,301],{"class":122,"line":281},3,[255,283,284],{"class":264},"parcels ",[255,286,287],{"class":260},"=",[255,289,290],{"class":264}," QgsProject.instance().mapLayersByName(",[255,292,294],{"class":293},"sU2Wk","\"parcels\"",[255,296,297],{"class":264},")[",[255,299,59],{"class":300},"sDLfK",[255,302,303],{"class":264},"]\n",[255,305,307,310,312,315,318],{"class":122,"line":306},4,[255,308,309],{"class":264},"index ",[255,311,287],{"class":260},[255,313,314],{"class":264}," parcels.fields().indexOf(",[255,316,317],{"class":293},"\"status\"",[255,319,320],{"class":264},")\n",[255,322,324],{"class":122,"line":323},5,[255,325,278],{"emptyLinePlaceholder":277},[255,327,329,332,335],{"class":122,"line":328},6,[255,330,331],{"class":260},"if",[255,333,334],{"class":260}," not",[255,336,337],{"class":264}," parcels.startEditing():\n",[255,339,341,344,347,350,353],{"class":122,"line":340},7,[255,342,343],{"class":260},"    raise",[255,345,346],{"class":300}," SystemExit",[255,348,349],{"class":264},"(",[255,351,352],{"class":293},"\"layer is not editable\"",[255,354,320],{"class":264},[255,356,358],{"class":122,"line":357},8,[255,359,278],{"emptyLinePlaceholder":277},[255,361,363,366],{"class":122,"line":362},9,[255,364,365],{"class":260},"try",[255,367,368],{"class":264},":\n",[255,370,372,375,378,381,384,387],{"class":122,"line":371},10,[255,373,374],{"class":260},"    for",[255,376,377],{"class":264}," feature ",[255,379,380],{"class":260},"in",[255,382,383],{"class":264}," parcels.getFeatures(",[255,385,386],{"class":293},"'\"area_m2\" > 5000'",[255,388,389],{"class":264},"):\n",[255,391,393,396,399],{"class":122,"line":392},11,[255,394,395],{"class":264},"        parcels.changeAttributeValue(feature.id(), index, ",[255,397,398],{"class":293},"\"large\"",[255,400,320],{"class":264},[255,402,404,407,409],{"class":122,"line":403},12,[255,405,406],{"class":260},"    if",[255,408,334],{"class":260},[255,410,411],{"class":264}," parcels.commitChanges():\n",[255,413,415,418,421,423,426],{"class":122,"line":414},13,[255,416,417],{"class":260},"        raise",[255,419,420],{"class":300}," RuntimeError",[255,422,349],{"class":264},[255,424,425],{"class":293},"\"; \"",[255,427,428],{"class":264},".join(parcels.commitErrors()))\n",[255,430,432,435,438],{"class":122,"line":431},14,[255,433,434],{"class":260},"except",[255,436,437],{"class":300}," Exception",[255,439,368],{"class":264},[255,441,443],{"class":122,"line":442},15,[255,444,445],{"class":264},"    parcels.rollBack()\n",[255,447,449],{"class":122,"line":448},16,[255,450,451],{"class":260},"    raise\n",[14,453,454,457,458,461,462,465,466,468,469,472,473,475,476,478,479,481,482,485],{},[227,455,456],{},"Breakdown:"," ",[18,459,460],{},"startEditing()"," returns ",[18,463,464],{},"False"," rather than raising when the layer cannot be edited, so the check earns its place — a read-only source otherwise produces a loop that appears to work and commits nothing. ",[18,467,152],{}," also returns a boolean, and ",[18,470,471],{},"commitErrors()"," holds the reasons, which are usually a constraint violation or a provider that rejected a type. The ",[18,474,365],{},"\u002F",[18,477,434],{}," with ",[18,480,162],{}," is what makes this safe: without it, an exception halfway through leaves the layer in edit mode with a partial buffer, and the next code to touch it inherits that state. Passing an expression to ",[18,483,484],{},"getFeatures"," filters at the provider, so only the matching features are fetched.",[14,487,488],{},"Rolling back discards the buffer entirely — it is not a partial undo, and there is no way to keep some changes and drop others except by not making them.",[216,490,492],{"id":491},"the-provider-path","The provider path",[246,494,496],{"className":248,"code":495,"language":250,"meta":251,"style":251},"provider = parcels.dataProvider()\n\nupdates = {}\nfor feature in parcels.getFeatures('\"area_m2\" > 5000'):\n    updates[feature.id()] = {index: \"large\"}\n\nprovider.changeAttributeValues(updates)\n",[18,497,498,508,512,522,537,552,556],{"__ignoreMap":251},[255,499,500,503,505],{"class":122,"line":257},[255,501,502],{"class":264},"provider ",[255,504,287],{"class":260},[255,506,507],{"class":264}," parcels.dataProvider()\n",[255,509,510],{"class":122,"line":274},[255,511,278],{"emptyLinePlaceholder":277},[255,513,514,517,519],{"class":122,"line":281},[255,515,516],{"class":264},"updates ",[255,518,287],{"class":260},[255,520,521],{"class":264}," {}\n",[255,523,524,527,529,531,533,535],{"class":122,"line":306},[255,525,526],{"class":260},"for",[255,528,377],{"class":264},[255,530,380],{"class":260},[255,532,383],{"class":264},[255,534,386],{"class":293},[255,536,389],{"class":264},[255,538,539,542,544,547,549],{"class":122,"line":323},[255,540,541],{"class":264},"    updates[feature.id()] ",[255,543,287],{"class":260},[255,545,546],{"class":264}," {index: ",[255,548,398],{"class":293},[255,550,551],{"class":264},"}\n",[255,553,554],{"class":122,"line":328},[255,555,278],{"emptyLinePlaceholder":277},[255,557,558],{"class":122,"line":340},[255,559,560],{"class":264},"provider.changeAttributeValues(updates)\n",[14,562,563,565],{},[227,564,456],{}," One call carrying a dictionary of feature id to attribute changes is dramatically faster than a loop of individual writes, because the provider batches them into one statement or one file operation. There is no buffer, so the change is on disk when the call returns and there is nothing to commit. There is also nothing to undo, no signal that the layer changed in the way the buffer emits, and no participation in a transaction — which is exactly why this belongs in a script working on a copy and not in a plugin operating on a user's data.",[14,567,568],{},"Geometry is the same shape:",[246,570,572],{"className":248,"code":571,"language":250,"meta":251,"style":251},"from qgis.core import QgsGeometry\n\ngeometries = {\n    feature.id(): feature.geometry().buffer(2.0, 8)\n    for feature in parcels.getFeatures()\n}\nprovider.changeGeometryValues(geometries)\n",[18,573,574,585,589,599,614,625,629],{"__ignoreMap":251},[255,575,576,578,580,582],{"class":122,"line":257},[255,577,261],{"class":260},[255,579,265],{"class":264},[255,581,268],{"class":260},[255,583,584],{"class":264}," QgsGeometry\n",[255,586,587],{"class":122,"line":274},[255,588,278],{"emptyLinePlaceholder":277},[255,590,591,594,596],{"class":122,"line":281},[255,592,593],{"class":264},"geometries ",[255,595,287],{"class":260},[255,597,598],{"class":264}," {\n",[255,600,601,604,607,610,612],{"class":122,"line":306},[255,602,603],{"class":264},"    feature.id(): feature.geometry().buffer(",[255,605,606],{"class":300},"2.0",[255,608,609],{"class":264},", ",[255,611,71],{"class":300},[255,613,320],{"class":264},[255,615,616,618,620,622],{"class":122,"line":323},[255,617,374],{"class":260},[255,619,377],{"class":264},[255,621,380],{"class":260},[255,623,624],{"class":264}," parcels.getFeatures()\n",[255,626,627],{"class":122,"line":328},[255,628,551],{"class":264},[255,630,631],{"class":122,"line":340},[255,632,633],{"class":264},"provider.changeGeometryValues(geometries)\n",[14,635,636,638,639,642],{},[227,637,456],{}," Building the whole dictionary before writing costs memory proportional to the layer, so on a very large layer chunk it into batches of a few thousand. ",[18,640,641],{},"buffer(distance, segments)"," returns a new geometry rather than modifying in place, which is why the comprehension works. Note that this bypasses any geometry validity checking the layer might otherwise apply.",[216,644,646],{"id":645},"performance-and-why-loops-are-slow","Performance and why loops are slow",[14,648,649],{},[42,650,653,656,659,662,665,670,673,678,684,688,691,694,699,705,708,712,715],{"viewBox":651,"role":45,"ariaLabel":652,"xmlns":47},"0 0 760 288","The cost of three write patterns: one commit per feature, one commit for the whole run, and a single batched provider call",[49,654,655],{},"Where the time actually goes",[53,657,658],{},"Committing after every feature pays the provider's write cost once per feature and is orders of magnitude slower. One commit at the end of an edit session pays it once. A single batched provider call is faster still, at the cost of losing undo and rollback.",[57,660],{"x":59,"y":59,"width":60,"height":661,"fill":62},"288",[81,663,664],{"x":83,"y":84,"style":85,"fill":86,"textAnchor":87},"10,000 attribute changes",[122,666],{"x1":667,"y1":668,"x2":667,"y2":669,"stroke":79,"style":114},"150","60","216",[122,671],{"x1":667,"y1":669,"x2":672,"y2":669,"stroke":79,"style":114},"716",[57,674],{"x":675,"y":102,"width":676,"height":110,"rx":677,"fill":179,"stroke":171,"style":210},"156","540","4",[81,679,683],{"x":131,"y":680,"style":681,"fill":79,"textAnchor":682},"94","text-anchor:end;font-size:10px;font-family:sans-serif","end","commit per feature",[81,685,687],{"x":686,"y":680,"style":681,"fill":171,"textAnchor":682},"684","minutes",[57,689],{"x":675,"y":207,"width":690,"height":110,"rx":677,"fill":112,"stroke":113,"style":210},"96",[81,692,693],{"x":131,"y":131,"style":681,"fill":79,"textAnchor":682},"one commit at the end",[81,695,698],{"x":696,"y":131,"style":697,"fill":113},"266","font-size:10px;font-family:sans-serif","a few seconds",[57,700],{"x":675,"y":701,"width":702,"height":110,"rx":677,"fill":703,"stroke":704,"style":210},"168","44","#dbeafe","#2563eb",[81,706,707],{"x":131,"y":126,"style":681,"fill":79,"textAnchor":682},"batched provider call",[81,709,711],{"x":710,"y":126,"style":697,"fill":704},"214","under a second",[57,713],{"x":207,"y":155,"width":209,"height":714,"rx":71,"fill":157,"stroke":158,"style":210},"40",[81,716,718],{"x":83,"y":717,"style":196,"fill":79,"textAnchor":87},"263","the middle one is usually the right trade — fast enough, and undoable",[14,720,721],{},"The pattern that ruins performance is committing inside the loop. Each commit flushes to the provider, and on a database that is a round trip per feature. Keeping the edit session open for the whole loop and committing once turns thousands of round trips into one.",[14,723,724],{},"The second cost is fetching more than you need:",[246,726,728],{"className":248,"code":727,"language":250,"meta":251,"style":251},"from qgis.core import QgsFeatureRequest\n\nrequest = QgsFeatureRequest()\nrequest.setFlags(QgsFeatureRequest.NoGeometry)\nrequest.setSubsetOfAttributes([\"area_m2\"], parcels.fields())\n\nfor feature in parcels.getFeatures(request):\n    ...\n",[18,729,730,741,745,755,760,771,775,786],{"__ignoreMap":251},[255,731,732,734,736,738],{"class":122,"line":257},[255,733,261],{"class":260},[255,735,265],{"class":264},[255,737,268],{"class":260},[255,739,740],{"class":264}," QgsFeatureRequest\n",[255,742,743],{"class":122,"line":274},[255,744,278],{"emptyLinePlaceholder":277},[255,746,747,750,752],{"class":122,"line":281},[255,748,749],{"class":264},"request ",[255,751,287],{"class":260},[255,753,754],{"class":264}," QgsFeatureRequest()\n",[255,756,757],{"class":122,"line":306},[255,758,759],{"class":264},"request.setFlags(QgsFeatureRequest.NoGeometry)\n",[255,761,762,765,768],{"class":122,"line":323},[255,763,764],{"class":264},"request.setSubsetOfAttributes([",[255,766,767],{"class":293},"\"area_m2\"",[255,769,770],{"class":264},"], parcels.fields())\n",[255,772,773],{"class":122,"line":328},[255,774,278],{"emptyLinePlaceholder":277},[255,776,777,779,781,783],{"class":122,"line":340},[255,778,526],{"class":260},[255,780,377],{"class":264},[255,782,380],{"class":260},[255,784,785],{"class":264}," parcels.getFeatures(request):\n",[255,787,788],{"class":122,"line":357},[255,789,790],{"class":300},"    ...\n",[14,792,793,457,795,798,799,802,803,240],{},[227,794,456],{},[18,796,797],{},"NoGeometry"," skips decoding geometry that an attribute-only update will never look at, which on a layer of complex polygons is most of the read time. ",[18,800,801],{},"setSubsetOfAttributes"," fetches only the columns named. Together they routinely halve the time of an attribute pass, and the details are covered in ",[34,804,806],{"href":805},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","speeding up feature iteration with QgsFeatureRequest",[216,808,810],{"id":809},"constraints-and-why-a-commit-fails","Constraints and why a commit fails",[14,812,813,814,816],{},"A commit that returns ",[18,815,464],{}," is nearly always a constraint the provider enforced and the buffer did not. QGIS applies field constraints — not null, unique, an expression — at the buffer level only if they are declared on the layer; the underlying database applies its own regardless.",[246,818,820],{"className":248,"code":819,"language":250,"meta":251,"style":251},"from qgis.core import QgsFieldConstraints\n\nfor field in parcels.fields():\n    constraints = field.constraints().constraints()\n    if constraints:\n        print(field.name(),\n              \"not-null\" if constraints & QgsFieldConstraints.ConstraintNotNull else \"\",\n              \"unique\" if constraints & QgsFieldConstraints.ConstraintUnique else \"\",\n              field.constraints().constraintExpression() or \"\")\n",[18,821,822,833,837,849,859,866,874,900,920],{"__ignoreMap":251},[255,823,824,826,828,830],{"class":122,"line":257},[255,825,261],{"class":260},[255,827,265],{"class":264},[255,829,268],{"class":260},[255,831,832],{"class":264}," QgsFieldConstraints\n",[255,834,835],{"class":122,"line":274},[255,836,278],{"emptyLinePlaceholder":277},[255,838,839,841,844,846],{"class":122,"line":281},[255,840,526],{"class":260},[255,842,843],{"class":264}," field ",[255,845,380],{"class":260},[255,847,848],{"class":264}," parcels.fields():\n",[255,850,851,854,856],{"class":122,"line":306},[255,852,853],{"class":264},"    constraints ",[255,855,287],{"class":260},[255,857,858],{"class":264}," field.constraints().constraints()\n",[255,860,861,863],{"class":122,"line":323},[255,862,406],{"class":260},[255,864,865],{"class":264}," constraints:\n",[255,867,868,871],{"class":122,"line":328},[255,869,870],{"class":300},"        print",[255,872,873],{"class":264},"(field.name(),\n",[255,875,876,879,882,885,888,891,894,897],{"class":122,"line":340},[255,877,878],{"class":293},"              \"not-null\"",[255,880,881],{"class":260}," if",[255,883,884],{"class":264}," constraints ",[255,886,887],{"class":260},"&",[255,889,890],{"class":264}," QgsFieldConstraints.ConstraintNotNull ",[255,892,893],{"class":260},"else",[255,895,896],{"class":293}," \"\"",[255,898,899],{"class":264},",\n",[255,901,902,905,907,909,911,914,916,918],{"class":122,"line":357},[255,903,904],{"class":293},"              \"unique\"",[255,906,881],{"class":260},[255,908,884],{"class":264},[255,910,887],{"class":260},[255,912,913],{"class":264}," QgsFieldConstraints.ConstraintUnique ",[255,915,893],{"class":260},[255,917,896],{"class":293},[255,919,899],{"class":264},[255,921,922,925,928,930],{"class":122,"line":362},[255,923,924],{"class":264},"              field.constraints().constraintExpression() ",[255,926,927],{"class":260},"or",[255,929,896],{"class":293},[255,931,320],{"class":264},[14,933,934,936,937,939,940,942],{},[227,935,456],{}," Constraints are a bit field, so testing with ",[18,938,887],{}," rather than equality is what catches a field carrying several. Printing them before a bulk update tells you which values will be rejected, and it is far cheaper than discovering it in ",[18,941,471],{}," after a long session. A constraint declared on the layer is checked as you edit and shows in the form; one that exists only in the database appears at commit and takes the whole session down with it, which is a strong argument for declaring them on the layer too.",[14,944,945,946,949,950,953],{},"The other frequent commit failure is a type mismatch — writing a Python ",[18,947,948],{},"str"," into an integer field, or a ",[18,951,952],{},"float"," into a field the provider declared as integer. The buffer accepts it, the provider rejects it, and the message names the field. Casting explicitly at the point of assignment removes the whole class of problem.",[216,955,957],{"id":956},"transactions-across-several-layers","Transactions across several layers",[14,959,960],{},"For database-backed layers, QGIS can group edits so that several layers commit or roll back together — which is what you need when a change to a parcel and a change to its inspections must both succeed or both fail.",[246,962,964],{"className":248,"code":963,"language":250,"meta":251,"style":251},"project = QgsProject.instance()\nproject.setTransactionMode(Qgis.TransactionMode.AutomaticGroups)\n",[18,965,966,976],{"__ignoreMap":251},[255,967,968,971,973],{"class":122,"line":257},[255,969,970],{"class":264},"project ",[255,972,287],{"class":260},[255,974,975],{"class":264}," QgsProject.instance()\n",[255,977,978],{"class":122,"line":274},[255,979,980],{"class":264},"project.setTransactionMode(Qgis.TransactionMode.AutomaticGroups)\n",[14,982,983,985,986,240],{},[227,984,456],{}," With automatic transaction groups enabled, starting an edit session on one PostGIS layer opens a database transaction shared by every layer from the same connection, and committing commits all of them. It only works for providers that support transactions — PostGIS does, GeoPackage does in recent releases, shapefile does not — and it changes the editing behaviour users see, so a plugin should not enable it behind their back. On 3.24 and earlier the equivalent is ",[18,987,988],{},"project.setAutoTransaction(True)",[14,990,991],{},"Where the layers do not share a connection, there is no cross-layer transaction and the honest approach is to order the writes so the least damaging one fails first, and to report clearly if a later one does.",[216,993,995],{"id":994},"watching-what-the-buffer-holds","Watching what the buffer holds",[14,997,998],{},"An edit session is inspectable, which is useful both for debugging and for reporting to a user before a commit.",[246,1000,1002],{"className":248,"code":1001,"language":250,"meta":251,"style":251},"print(\"added:\", len(parcels.editBuffer().addedFeatures()))\nprint(\"changed attributes on:\", len(parcels.editBuffer().changedAttributeValues()))\nprint(\"changed geometry on:\", len(parcels.editBuffer().changedGeometries()))\nprint(\"deleted:\", len(parcels.editBuffer().deletedFeatureIds()))\n",[18,1003,1004,1022,1038,1054],{"__ignoreMap":251},[255,1005,1006,1009,1011,1014,1016,1019],{"class":122,"line":257},[255,1007,1008],{"class":300},"print",[255,1010,349],{"class":264},[255,1012,1013],{"class":293},"\"added:\"",[255,1015,609],{"class":264},[255,1017,1018],{"class":300},"len",[255,1020,1021],{"class":264},"(parcels.editBuffer().addedFeatures()))\n",[255,1023,1024,1026,1028,1031,1033,1035],{"class":122,"line":274},[255,1025,1008],{"class":300},[255,1027,349],{"class":264},[255,1029,1030],{"class":293},"\"changed attributes on:\"",[255,1032,609],{"class":264},[255,1034,1018],{"class":300},[255,1036,1037],{"class":264},"(parcels.editBuffer().changedAttributeValues()))\n",[255,1039,1040,1042,1044,1047,1049,1051],{"class":122,"line":281},[255,1041,1008],{"class":300},[255,1043,349],{"class":264},[255,1045,1046],{"class":293},"\"changed geometry on:\"",[255,1048,609],{"class":264},[255,1050,1018],{"class":300},[255,1052,1053],{"class":264},"(parcels.editBuffer().changedGeometries()))\n",[255,1055,1056,1058,1060,1063,1065,1067],{"class":122,"line":306},[255,1057,1008],{"class":300},[255,1059,349],{"class":264},[255,1061,1062],{"class":293},"\"deleted:\"",[255,1064,609],{"class":264},[255,1066,1018],{"class":300},[255,1068,1069],{"class":264},"(parcels.editBuffer().deletedFeatureIds()))\n",[14,1071,1072,457,1074,461,1077,1080,1081,1083],{},[227,1073,456],{},[18,1075,1076],{},"editBuffer()",[18,1078,1079],{},"None"," when the layer is not in edit mode, so guard on ",[18,1082,236],{}," before calling any of these. The four collections are exactly what will be sent on commit, which makes them the right thing to summarise in a confirmation dialog — \"this will update 412 parcels and delete 3\" is far more useful than a bare \"commit?\". Added features carry negative ids until they are committed, which is worth knowing because any code correlating buffered features with stored ones has to account for the renumbering that happens at commit time.",[14,1085,1086,1087,609,1090,609,1093,609,1096,1099,1100,1104],{},"The layer also emits signals for each of these — ",[18,1088,1089],{},"attributeValueChanged",[18,1091,1092],{},"geometryChanged",[18,1094,1095],{},"featureAdded",[18,1097,1098],{},"featureDeleted"," — which is how a panel stays in step with an ongoing edit session without polling. Connect to them with the same care about disconnecting described in ",[34,1101,1103],{"href":1102},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-object-ownership-and-crashes-pyqgis\u002F","object ownership and crashes",", since an edit session outlives most of the code that starts it.",[216,1106,1108],{"id":1107},"qgis-version-compatibility","QGIS version compatibility",[14,1110,1111,1112,1115,1116,1119,1120,1123,1124,1115,1127,1130],{},"The edit buffer API has been stable across QGIS 3. ",[18,1113,1114],{},"QgsVectorLayer.setTransactionMode"," and ",[18,1117,1118],{},"Qgis.TransactionMode"," replaced ",[18,1121,1122],{},"setAutoTransaction"," in 3.26, with the older method retained. GeoPackage transaction support improved through 3.26 to 3.34. ",[18,1125,1126],{},"changeAttributeValues",[18,1128,1129],{},"changeGeometryValues"," on the provider have been present throughout.",[216,1132,1134],{"id":1133},"troubleshooting","Troubleshooting",[221,1136,1137,1145,1155,1161,1167,1173],{},[224,1138,1139,1144],{},[227,1140,1141,1143],{},[18,1142,460],{}," returns False."," The source is read-only, or a file lock is held by another process.",[224,1146,1147,1151,1152,1154],{},[227,1148,1149,1143],{},[18,1150,152],{}," Read ",[18,1153,471],{}," — usually a not-null constraint or a type mismatch.",[224,1156,1157,1160],{},[227,1158,1159],{},"The layer stays in edit mode after an error."," No rollback in the exception path.",[224,1162,1163,1166],{},[227,1164,1165],{},"Changes vanish on project close."," They were staged and never committed.",[224,1168,1169,1172],{},[227,1170,1171],{},"A provider write had no effect."," The field index was wrong; there is no error for writing to an index that does not exist on some providers.",[224,1174,1175,1178],{},[227,1176,1177],{},"Edits are extremely slow."," A commit inside the loop, or geometry being fetched for an attribute-only pass.",[216,1180,1182],{"id":1181},"conclusion","Conclusion",[14,1184,1185],{},"Use the edit buffer whenever a person or a plugin is involved, wrap it in a try\u002Fexcept with a rollback, and commit once rather than per feature. Drop to the provider only for bulk work on data you can regenerate, and remember that it gives up undo entirely. On a database, transaction groups extend the same guarantee across related layers, which is what makes multi-layer edits safe.",[216,1187,1189],{"id":1188},"frequently-asked-questions","Frequently Asked Questions",[14,1191,1192,1195],{},[227,1193,1194],{},"Can I undo a provider write?","\nNo. That is the trade — no buffer, no undo stack, no rollback. Work on a copy if the result might be wrong.",[14,1197,1198,1204,1205,1208],{},[227,1199,1200,1201,1203],{},"Does ",[18,1202,24],{}," keep the layer in edit mode?","\nNo by default; pass ",[18,1206,1207],{},"stopEditing=False"," to commit and continue editing, which is useful for checkpointing a long session.",[14,1210,1211,1214,1217,1218,1221],{},[227,1212,1213],{},"How do I add features rather than change them?",[18,1215,1216],{},"layer.addFeature(feature)"," inside an edit session, or ",[18,1219,1220],{},"provider.addFeatures([...])"," for the direct path. Both need the feature's fields to match the layer's.",[14,1223,1224,1227,1228,1231],{},[227,1225,1226],{},"Why does my change not appear on the canvas?","\nCall ",[18,1229,1230],{},"layer.triggerRepaint()"," after a provider write; the buffer path emits signals that refresh the canvas on its own.",[216,1233,1235],{"id":1234},"related","Related",[221,1237,1238,1244,1250,1255,1261],{},[224,1239,1240,1243],{},[34,1241,1242],{"href":36},"Vector Data Manipulation in PyQGIS"," — the guide this recipe belongs to",[224,1245,1246],{},[34,1247,1249],{"href":1248},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in PyQGIS",[224,1251,1252],{},[34,1253,1254],{"href":805},"Speed Up Feature Iteration with QgsFeatureRequest",[224,1256,1257],{},[34,1258,1260],{"href":1259},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Ffix-invalid-geometries-pyqgis\u002F","Fix Invalid Geometries in PyQGIS",[224,1262,1263],{},[34,1264,1266],{"href":1265},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fconnect-to-postgis-database-pyqgis\u002F","Connect to a PostGIS Database in PyQGIS",[1268,1269,1270],"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);}",{"title":251,"searchDepth":274,"depth":274,"links":1272},[1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284],{"id":218,"depth":274,"text":219},{"id":243,"depth":274,"text":244},{"id":491,"depth":274,"text":492},{"id":645,"depth":274,"text":646},{"id":809,"depth":274,"text":810},{"id":956,"depth":274,"text":957},{"id":994,"depth":274,"text":995},{"id":1107,"depth":274,"text":1108},{"id":1133,"depth":274,"text":1134},{"id":1181,"depth":274,"text":1182},{"id":1188,"depth":274,"text":1189},{"id":1234,"depth":274,"text":1235},"Change geometry and attributes safely — the edit buffer versus direct provider writes, commit and rollback, undo stacks, and database transaction groups across several layers.","md",{"slug":1288,"type":1289,"breadcrumb":1290,"datePublished":1291,"dateModified":1291},"edit-features-with-transactions-pyqgis","article","Edit with Transactions","2026-09-05","\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis",{"title":5,"description":1285},"spatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis\u002Findex","w0ROETB5cwO0F86lOorGzZrK7itFKH8KKHjH9l5CSj4",1788563856791]