[{"data":1,"prerenderedAt":1721},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fundo-edits-with-edit-buffer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1710,"extension":1711,"meta":1712,"navigation":259,"path":1717,"seo":1718,"stem":1719,"__hash__":1720},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fundo-edits-with-edit-buffer-pyqgis\u002Findex.md","Undo Edits with the Edit Buffer in PyQGIS",{"type":7,"value":8,"toc":1696},"minimark",[9,13,17,26,186,191,220,224,227,441,463,475,479,486,563,686,702,706,709,942,963,967,974,1057,1221,1234,1238,1241,1343,1352,1356,1359,1512,1541,1545,1573,1577,1611,1615,1625,1629,1635,1644,1653,1659,1663,1692],[10,11,5],"h1",{"id":12},"undo-edits-with-the-edit-buffer-in-pyqgis",[14,15,16],"p",{},"When a vector layer is in edit mode, changes do not go to the data source straight away. They collect in the layer's edit buffer: added features, deleted ids, changed attributes and changed geometries, each recorded on an undo stack. The data source sees them only when the edits are committed. This is what lets a person digitize for an hour, undo the last three steps, and then save — and it is what a plugin should use whenever its changes happen alongside the user's own.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002F","Features, Geometries & Memory Layers",". It drives the edit buffer from Python: opening and closing edit sessions, grouping a script's changes into one undo step, looking at pending changes before they are saved, and handling a commit that fails.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,103,109,117,122,126,130,134,138,143,147,154,160,164,168,172,176,181],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 254","img","States of an edit session: read mode, editing with a buffer and undo stack, then commit to the data source or roll back, with a failed commit leaving the buffer intact","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Life of an edit session",[40,41,42],"desc",{},"The layer starts in read mode. startEditing creates an edit buffer and an undo stack. Each change is recorded in both. commitChanges writes all changes to the data source in one go and clears the buffer; rollBack discards them. If the commit fails, the layer stays in edit mode with the buffer intact and commitErrors explains why.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","254","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"bufStateArrow","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","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Changes wait in the buffer until commit",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"24","110","150","70","#fffdf7","#59645f","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":73,"textAnchor":74},"99","138.805","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","read mode",[68,92,95],{"x":87,"y":93,"style":94,"fill":83,"textAnchor":74},"158.755","text-anchor:middle;font-size:10.5px;font-family:sans-serif","isEditable() False",[97,98],"line",{"x1":99,"y1":100,"x2":101,"y2":100,"stroke":66,"style":102},"174","145","214","stroke-width:1.8;marker-end:url(#bufStateArrow)",[68,104,108],{"x":105,"y":106,"style":107,"fill":83,"textAnchor":74},"194","132","text-anchor:middle;font-size:10.0px;font-family:sans-serif","start",[44,110],{"x":111,"y":112,"width":113,"height":114,"rx":58,"fill":115,"stroke":116,"style":84},"222","64","260","162","#eff3ff","#2563eb",[68,118,121],{"x":119,"y":120,"style":89,"fill":116,"textAnchor":74},"352","108.88","editing",[68,123,125],{"x":119,"y":124,"style":94,"fill":66,"textAnchor":74},"128.83","edit buffer",[68,127,129],{"x":119,"y":128,"style":94,"fill":66,"textAnchor":74},"148.78","added · deleted · changed",[68,131,133],{"x":119,"y":132,"style":94,"fill":66,"textAnchor":74},"168.73","undo stack",[68,135,137],{"x":119,"y":136,"style":94,"fill":83,"textAnchor":74},"188.68","one entry per command",[97,139],{"x1":140,"y1":79,"x2":141,"y2":142,"stroke":66,"style":102},"482","536","84",[97,144],{"x1":140,"y1":145,"x2":141,"y2":146,"stroke":66,"style":102},"180","206",[44,148],{"x":149,"y":150,"width":151,"height":112,"rx":58,"fill":152,"stroke":153,"style":84},"544","52","192","#e8efe6","#15803d",[68,155,159],{"x":156,"y":157,"style":89,"fill":158,"textAnchor":74},"640","77.805","#166534","commitChanges()",[68,161,163],{"x":156,"y":162,"style":94,"fill":83,"textAnchor":74},"97.755","written, buffer cleared",[44,165],{"x":149,"y":99,"width":151,"height":112,"rx":58,"fill":166,"stroke":167,"style":84},"#fdf2e2","#b45309",[68,169,171],{"x":156,"y":170,"style":89,"fill":167,"textAnchor":74},"199.805","rollBack()",[68,173,175],{"x":156,"y":174,"style":94,"fill":83,"textAnchor":74},"219.755","changes discarded",[44,177],{"x":149,"y":178,"width":151,"height":179,"rx":58,"fill":166,"stroke":180,"style":84},"128","38","#b91c1c",[68,182,185],{"x":156,"y":183,"style":184,"fill":66,"textAnchor":74},"150.42","text-anchor:middle;font-size:9.5px;font-family:sans-serif","commit failed → stays editing",[187,188,190],"h2",{"id":189},"prerequisites","Prerequisites",[192,193,194,198,217],"ul",{},[195,196,197],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series.",[195,199,200,201,205,206,209,210,209,213,216],{},"An editable layer — check that ",[202,203,204],"code",{},"layer.dataProvider().capabilities()"," includes the capability you need (",[202,207,208],{},"ChangeAttributeValues",", ",[202,211,212],{},"ChangeGeometries",[202,214,215],{},"DeleteFeatures",").",[195,218,219],{},"Code running on the main thread. The edit buffer is not thread-safe; background work should compute changes and apply them on the main thread.",[187,221,223],{"id":222},"start-commit-and-roll-back","Start, commit and roll back",[14,225,226],{},"The three basic calls map directly to the buttons in the Digitizing toolbar. Each returns a boolean, and each should be checked.",[228,229,234],"pre",{"className":230,"code":231,"language":232,"meta":233,"style":233},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject\n\npipes = QgsProject.instance().mapLayersByName(\"water_pipes\")[0]\nstatus_idx = pipes.fields().indexOf(\"status\")\n\nif not pipes.isEditable() and not pipes.startEditing():\n    raise RuntimeError(\"layer cannot be edited\")\n\nfor f in pipes.getFeatures('\"install_year\" \u003C 1960'):\n    pipes.changeAttributeValue(f.id(), status_idx, \"review\")\n\nprint(pipes.isModified(), \"pending changes\")\n\nif not pipes.commitChanges():\n    print(\"\\n\".join(pipes.commitErrors()))\n    pipes.rollBack()\n","python","",[202,235,236,254,261,286,303,308,328,345,350,371,382,387,401,406,416,435],{"__ignoreMap":233},[237,238,240,244,248,251],"span",{"class":97,"line":239},1,[237,241,243],{"class":242},"snl16","from",[237,245,247],{"class":246},"s95oV"," qgis.core ",[237,249,250],{"class":242},"import",[237,252,253],{"class":246}," QgsProject\n",[237,255,257],{"class":97,"line":256},2,[237,258,260],{"emptyLinePlaceholder":259},true,"\n",[237,262,264,267,270,273,277,280,283],{"class":97,"line":263},3,[237,265,266],{"class":246},"pipes ",[237,268,269],{"class":242},"=",[237,271,272],{"class":246}," QgsProject.instance().mapLayersByName(",[237,274,276],{"class":275},"sU2Wk","\"water_pipes\"",[237,278,279],{"class":246},")[",[237,281,46],{"class":282},"sDLfK",[237,284,285],{"class":246},"]\n",[237,287,289,292,294,297,300],{"class":97,"line":288},4,[237,290,291],{"class":246},"status_idx ",[237,293,269],{"class":242},[237,295,296],{"class":246}," pipes.fields().indexOf(",[237,298,299],{"class":275},"\"status\"",[237,301,302],{"class":246},")\n",[237,304,306],{"class":97,"line":305},5,[237,307,260],{"emptyLinePlaceholder":259},[237,309,311,314,317,320,323,325],{"class":97,"line":310},6,[237,312,313],{"class":242},"if",[237,315,316],{"class":242}," not",[237,318,319],{"class":246}," pipes.isEditable() ",[237,321,322],{"class":242},"and",[237,324,316],{"class":242},[237,326,327],{"class":246}," pipes.startEditing():\n",[237,329,331,334,337,340,343],{"class":97,"line":330},7,[237,332,333],{"class":242},"    raise",[237,335,336],{"class":282}," RuntimeError",[237,338,339],{"class":246},"(",[237,341,342],{"class":275},"\"layer cannot be edited\"",[237,344,302],{"class":246},[237,346,348],{"class":97,"line":347},8,[237,349,260],{"emptyLinePlaceholder":259},[237,351,353,356,359,362,365,368],{"class":97,"line":352},9,[237,354,355],{"class":242},"for",[237,357,358],{"class":246}," f ",[237,360,361],{"class":242},"in",[237,363,364],{"class":246}," pipes.getFeatures(",[237,366,367],{"class":275},"'\"install_year\" \u003C 1960'",[237,369,370],{"class":246},"):\n",[237,372,374,377,380],{"class":97,"line":373},10,[237,375,376],{"class":246},"    pipes.changeAttributeValue(f.id(), status_idx, ",[237,378,379],{"class":275},"\"review\"",[237,381,302],{"class":246},[237,383,385],{"class":97,"line":384},11,[237,386,260],{"emptyLinePlaceholder":259},[237,388,390,393,396,399],{"class":97,"line":389},12,[237,391,392],{"class":282},"print",[237,394,395],{"class":246},"(pipes.isModified(), ",[237,397,398],{"class":275},"\"pending changes\"",[237,400,302],{"class":246},[237,402,404],{"class":97,"line":403},13,[237,405,260],{"emptyLinePlaceholder":259},[237,407,409,411,413],{"class":97,"line":408},14,[237,410,313],{"class":242},[237,412,316],{"class":242},[237,414,415],{"class":246}," pipes.commitChanges():\n",[237,417,419,422,424,427,430,432],{"class":97,"line":418},15,[237,420,421],{"class":282},"    print",[237,423,339],{"class":246},[237,425,426],{"class":275},"\"",[237,428,429],{"class":282},"\\n",[237,431,426],{"class":275},[237,433,434],{"class":246},".join(pipes.commitErrors()))\n",[237,436,438],{"class":97,"line":437},16,[237,439,440],{"class":246},"    pipes.rollBack()\n",[14,442,443,447,448,451,452,455,456,459,460,462],{},[444,445,446],"strong",{},"Breakdown:"," Checking ",[202,449,450],{},"isEditable()"," first avoids interfering with an edit session the user already started — calling ",[202,453,454],{},"startEditing()"," on a layer that is already editable simply returns False, which a careless script treats as failure. ",[202,457,458],{},"isModified()"," reports whether the buffer holds anything. ",[202,461,159],{}," writes everything in one provider call per change type and, on success, clears the buffer and leaves edit mode. On failure it returns False and keeps the layer editable with the buffer intact, so the script must decide between showing the errors to a user and rolling back.",[14,464,465,466,469,470,474],{},"For a script that should not leave a layer half-edited under any circumstances, the ",[202,467,468],{},"edit()"," context manager does all of this: it starts editing, commits at the end of the block, and rolls back if an exception escapes — the pattern used in ",[21,471,473],{"href":472},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fadd-features-to-vector-layer-pyqgis\u002F","adding features to a vector layer",".",[187,476,478],{"id":477},"group-a-scripts-changes-into-one-undo-step","Group a script's changes into one undo step",[14,480,481,482,485],{},"Without grouping, each ",[202,483,484],{},"changeAttributeValue"," becomes its own entry on the undo stack. A plugin that updates four hundred features leaves four hundred entries, and the user has to press Ctrl+Z four hundred times. Edit commands wrap a batch of changes into a single, named step.",[14,487,488],{},[29,489,492,495,498,501,508,511,516,521,525,529,533,537,540,544,547,551,554,557],{"viewBox":490,"role":32,"ariaLabel":491,"xmlns":34},"0 0 760 250","Without an edit command every change is its own undo entry; wrapping the loop in beginEditCommand and endEditCommand makes one named undo step",[36,493,494],{},"Edit commands and the undo stack",[40,496,497],{},"Without a command, each attribute change is a separate undo entry, so four hundred changes need four hundred undos. With beginEditCommand and endEditCommand around the loop, the same changes form one undo entry named, for example, Mark old pipes for review, which a single Ctrl+Z reverses. destroyEditCommand discards the partial command if the batch fails.",[44,499],{"x":46,"y":46,"width":47,"height":500,"fill":49},"250",[51,502,503],{},[54,504,506],{"id":505,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"bufCmdArrow",[63,507],{"d":65,"fill":66},[68,509,510],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"One click to undo the whole batch",[44,512],{"x":78,"y":513,"width":514,"height":515,"rx":58,"fill":166,"stroke":180,"style":84},"56","330","170",[68,517,520],{"x":518,"y":519,"style":89,"fill":180,"textAnchor":74},"189","104.88","no command",[68,522,524],{"x":518,"y":523,"style":94,"fill":66,"textAnchor":74},"124.83","change 1",[68,526,528],{"x":518,"y":527,"style":94,"fill":66,"textAnchor":74},"144.78","change 2",[68,530,532],{"x":518,"y":531,"style":94,"fill":66,"textAnchor":74},"164.73","… 398 more",[68,534,536],{"x":518,"y":535,"style":94,"fill":83,"textAnchor":74},"184.68","400 × Ctrl+Z",[44,538],{"x":539,"y":513,"width":514,"height":515,"rx":58,"fill":152,"stroke":153,"style":84},"406",[68,541,543],{"x":542,"y":519,"style":89,"fill":158,"textAnchor":74},"571","beginEditCommand(…)",[68,545,546],{"x":542,"y":523,"style":94,"fill":66,"textAnchor":74},"all 400 changes",[68,548,550],{"x":542,"y":527,"style":549,"fill":66,"textAnchor":74},"text-anchor:middle;font-size:10.0px;font-family:monospace","endEditCommand()",[68,552,553],{"x":542,"y":531,"style":94,"fill":66,"textAnchor":74},"\"Mark old pipes for review\"",[68,555,556],{"x":542,"y":535,"style":94,"fill":83,"textAnchor":74},"1 × Ctrl+Z",[97,558],{"x1":559,"y1":560,"x2":561,"y2":560,"stroke":66,"style":562},"354","141","402","stroke-width:1.8;marker-end:url(#bufCmdArrow)",[228,564,566],{"className":230,"code":565,"language":232,"meta":233,"style":233},"pipes.startEditing()\npipes.beginEditCommand(\"Mark old pipes for review\")\ntry:\n    for f in pipes.getFeatures('\"install_year\" \u003C 1960'):\n        pipes.changeAttributeValue(f.id(), status_idx, \"review\")\nexcept Exception:\n    pipes.destroyEditCommand()          # undo the partial batch\n    raise\nelse:\n    pipes.endEditCommand()\n\nstack = pipes.undoStack()\nprint(stack.count(), \"undo entries; top:\", stack.text(stack.index() - 1))\n",[202,567,568,573,582,590,605,614,624,633,638,645,650,654,664],{"__ignoreMap":233},[237,569,570],{"class":97,"line":239},[237,571,572],{"class":246},"pipes.startEditing()\n",[237,574,575,578,580],{"class":97,"line":256},[237,576,577],{"class":246},"pipes.beginEditCommand(",[237,579,553],{"class":275},[237,581,302],{"class":246},[237,583,584,587],{"class":97,"line":263},[237,585,586],{"class":242},"try",[237,588,589],{"class":246},":\n",[237,591,592,595,597,599,601,603],{"class":97,"line":288},[237,593,594],{"class":242},"    for",[237,596,358],{"class":246},[237,598,361],{"class":242},[237,600,364],{"class":246},[237,602,367],{"class":275},[237,604,370],{"class":246},[237,606,607,610,612],{"class":97,"line":305},[237,608,609],{"class":246},"        pipes.changeAttributeValue(f.id(), status_idx, ",[237,611,379],{"class":275},[237,613,302],{"class":246},[237,615,616,619,622],{"class":97,"line":310},[237,617,618],{"class":242},"except",[237,620,621],{"class":282}," Exception",[237,623,589],{"class":246},[237,625,626,629],{"class":97,"line":330},[237,627,628],{"class":246},"    pipes.destroyEditCommand()          ",[237,630,632],{"class":631},"sjoCn","# undo the partial batch\n",[237,634,635],{"class":97,"line":347},[237,636,637],{"class":242},"    raise\n",[237,639,640,643],{"class":97,"line":352},[237,641,642],{"class":242},"else",[237,644,589],{"class":246},[237,646,647],{"class":97,"line":373},[237,648,649],{"class":246},"    pipes.endEditCommand()\n",[237,651,652],{"class":97,"line":384},[237,653,260],{"emptyLinePlaceholder":259},[237,655,656,659,661],{"class":97,"line":389},[237,657,658],{"class":246},"stack ",[237,660,269],{"class":242},[237,662,663],{"class":246}," pipes.undoStack()\n",[237,665,666,668,671,674,677,680,683],{"class":97,"line":403},[237,667,392],{"class":282},[237,669,670],{"class":246},"(stack.count(), ",[237,672,673],{"class":275},"\"undo entries; top:\"",[237,675,676],{"class":246},", stack.text(stack.index() ",[237,678,679],{"class":242},"-",[237,681,682],{"class":282}," 1",[237,684,685],{"class":246},"))\n",[14,687,688,690,691,694,695,697,698,701],{},[444,689,446],{}," ",[202,692,693],{},"beginEditCommand(text)"," opens a macro on the layer's undo stack; every change until ",[202,696,550],{}," joins it. The text appears in QGIS's Undo\u002FRedo panel, so make it describe the action in the user's terms. ",[202,699,700],{},"destroyEditCommand()"," reverts the changes made since the command began and discards the command — the right response to an error halfway through, because it leaves the buffer exactly as it was before your code ran. The layer stays in edit mode afterwards, letting the user inspect the result and save or undo it with the normal tools.",[187,703,705],{"id":704},"inspect-pending-changes-before-saving","Inspect pending changes before saving",[14,707,708],{},"The edit buffer is readable. A plugin can show a summary — \"12 new, 3 deleted, 40 changed\" — or validate pending changes before committing them.",[228,710,712],{"className":230,"code":711,"language":232,"meta":233,"style":233},"buf = pipes.editBuffer()\nif buf is None:\n    print(\"layer is not in edit mode\")\nelse:\n    added = buf.addedFeatures()                 # {temp_id: QgsFeature}\n    deleted = buf.deletedFeatureIds()           # [fid, ...]\n    changed_attrs = buf.changedAttributeValues()  # {fid: {field_idx: value}}\n    changed_geoms = buf.changedGeometries()     # {fid: QgsGeometry}\n    print(f\"{len(added)} new, {len(deleted)} deleted, \"\n          f\"{len(changed_attrs)} with attribute edits, {len(changed_geoms)} reshaped\")\n\n    for fid, changes in list(changed_attrs.items())[:5]:\n        old = pipes.dataProvider().getFeatures(\n            QgsFeatureRequest().setFilterFid(fid)).__next__()\n        for idx, new in changes.items():\n            print(fid, pipes.fields()[idx].name(), old[idx], \"→\", new)\n",[202,713,714,724,739,750,756,769,782,795,808,841,870,874,894,904,915,928],{"__ignoreMap":233},[237,715,716,719,721],{"class":97,"line":239},[237,717,718],{"class":246},"buf ",[237,720,269],{"class":242},[237,722,723],{"class":246}," pipes.editBuffer()\n",[237,725,726,728,731,734,737],{"class":97,"line":256},[237,727,313],{"class":242},[237,729,730],{"class":246}," buf ",[237,732,733],{"class":242},"is",[237,735,736],{"class":282}," None",[237,738,589],{"class":246},[237,740,741,743,745,748],{"class":97,"line":263},[237,742,421],{"class":282},[237,744,339],{"class":246},[237,746,747],{"class":275},"\"layer is not in edit mode\"",[237,749,302],{"class":246},[237,751,752,754],{"class":97,"line":288},[237,753,642],{"class":242},[237,755,589],{"class":246},[237,757,758,761,763,766],{"class":97,"line":305},[237,759,760],{"class":246},"    added ",[237,762,269],{"class":242},[237,764,765],{"class":246}," buf.addedFeatures()                 ",[237,767,768],{"class":631},"# {temp_id: QgsFeature}\n",[237,770,771,774,776,779],{"class":97,"line":310},[237,772,773],{"class":246},"    deleted ",[237,775,269],{"class":242},[237,777,778],{"class":246}," buf.deletedFeatureIds()           ",[237,780,781],{"class":631},"# [fid, ...]\n",[237,783,784,787,789,792],{"class":97,"line":330},[237,785,786],{"class":246},"    changed_attrs ",[237,788,269],{"class":242},[237,790,791],{"class":246}," buf.changedAttributeValues()  ",[237,793,794],{"class":631},"# {fid: {field_idx: value}}\n",[237,796,797,800,802,805],{"class":97,"line":347},[237,798,799],{"class":246},"    changed_geoms ",[237,801,269],{"class":242},[237,803,804],{"class":246}," buf.changedGeometries()     ",[237,806,807],{"class":631},"# {fid: QgsGeometry}\n",[237,809,810,812,814,817,819,822,825,828,831,833,836,838],{"class":97,"line":352},[237,811,421],{"class":282},[237,813,339],{"class":246},[237,815,816],{"class":242},"f",[237,818,426],{"class":275},[237,820,821],{"class":282},"{len",[237,823,824],{"class":246},"(added)",[237,826,827],{"class":282},"}",[237,829,830],{"class":275}," new, ",[237,832,821],{"class":282},[237,834,835],{"class":246},"(deleted)",[237,837,827],{"class":282},[237,839,840],{"class":275}," deleted, \"\n",[237,842,843,846,848,850,853,855,858,860,863,865,868],{"class":97,"line":373},[237,844,845],{"class":242},"          f",[237,847,426],{"class":275},[237,849,821],{"class":282},[237,851,852],{"class":246},"(changed_attrs)",[237,854,827],{"class":282},[237,856,857],{"class":275}," with attribute edits, ",[237,859,821],{"class":282},[237,861,862],{"class":246},"(changed_geoms)",[237,864,827],{"class":282},[237,866,867],{"class":275}," reshaped\"",[237,869,302],{"class":246},[237,871,872],{"class":97,"line":384},[237,873,260],{"emptyLinePlaceholder":259},[237,875,876,878,881,883,886,889,891],{"class":97,"line":389},[237,877,594],{"class":242},[237,879,880],{"class":246}," fid, changes ",[237,882,361],{"class":242},[237,884,885],{"class":282}," list",[237,887,888],{"class":246},"(changed_attrs.items())[:",[237,890,59],{"class":282},[237,892,893],{"class":246},"]:\n",[237,895,896,899,901],{"class":97,"line":403},[237,897,898],{"class":246},"        old ",[237,900,269],{"class":242},[237,902,903],{"class":246}," pipes.dataProvider().getFeatures(\n",[237,905,906,909,912],{"class":97,"line":408},[237,907,908],{"class":246},"            QgsFeatureRequest().setFilterFid(fid)).",[237,910,911],{"class":282},"__next__",[237,913,914],{"class":246},"()\n",[237,916,917,920,923,925],{"class":97,"line":418},[237,918,919],{"class":242},"        for",[237,921,922],{"class":246}," idx, new ",[237,924,361],{"class":242},[237,926,927],{"class":246}," changes.items():\n",[237,929,930,933,936,939],{"class":97,"line":437},[237,931,932],{"class":282},"            print",[237,934,935],{"class":246},"(fid, pipes.fields()[idx].name(), old[idx], ",[237,937,938],{"class":275},"\"→\"",[237,940,941],{"class":246},", new)\n",[14,943,944,690,946,949,950,954,955,958,959,962],{},[444,945,446],{},[202,947,948],{},"editBuffer()"," returns None outside edit mode. Its four accessors give everything pending, keyed by feature id; added features carry negative temporary ids. Reading the ",[951,952,953],"em",{},"original"," value needs the data provider rather than the layer, because ",[202,956,957],{},"layer.getFeatures()"," returns features with the buffer's changes already applied — which is exactly what a user sees in the attribute table, and exactly what you do not want when building a before-and-after report. Add ",[202,960,961],{},"from qgis.core import QgsFeatureRequest"," if you run the snippet on its own.",[187,964,966],{"id":965},"handle-a-failed-commit","Handle a failed commit",[14,968,969,970,973],{},"Commits fail for reasons the buffer could not foresee: a database constraint, a lock held by another user, a dropped connection, a feature deleted by someone else since it was read. ",[202,971,972],{},"commitErrors()"," returns a list of human-readable messages.",[14,975,976],{},[29,977,980,983,986,989,996,999,1002,1006,1010,1014,1018,1022,1029,1032,1037,1042,1046,1049,1053],{"viewBox":978,"role":32,"ariaLabel":979,"xmlns":34},"0 0 760 232","After a failed commit, commitErrors lists what was not written and why; the script logs it and rolls back or hands the edit session back to the user",[36,981,982],{},"Reading commit errors",[40,984,985],{},"commitChanges returns False. commitErrors returns lines such as ERROR: 1 attribute value change(s) not committed, followed by the provider error, for example a foreign key violation or a not null constraint. The script logs the errors and either rolls back or leaves the layer editable for the user to fix the offending features and save again.",[44,987],{"x":46,"y":46,"width":47,"height":988,"fill":49},"232",[51,990,991],{},[54,992,994],{"id":993,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"bufFailArrow",[63,995],{"d":65,"fill":66},[68,997,998],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"When commitChanges() returns False",[44,1000],{"x":78,"y":513,"width":500,"height":1001,"rx":58,"fill":166,"stroke":180,"style":84},"160",[68,1003,972],{"x":1004,"y":1005,"style":89,"fill":180,"textAnchor":74},"149","99.88",[68,1007,1009],{"x":1004,"y":1008,"style":94,"fill":66,"textAnchor":74},"119.83","SUMMARY: 1 change",[68,1011,1013],{"x":1004,"y":1012,"style":94,"fill":66,"textAnchor":74},"139.78","not committed",[68,1015,1017],{"x":1004,"y":1016,"style":94,"fill":83,"textAnchor":74},"159.73","provider message:",[68,1019,1021],{"x":1004,"y":1020,"style":94,"fill":83,"textAnchor":74},"179.68","NOT NULL violated",[97,1023],{"x1":1024,"y1":1025,"x2":1026,"y2":1027,"stroke":66,"style":1028},"274","136","316","100","stroke-width:1.8;marker-end:url(#bufFailArrow)",[97,1030],{"x1":1024,"y1":1025,"x2":1026,"y2":1031,"stroke":66,"style":1028},"176",[44,1033],{"x":1034,"y":1035,"width":1036,"height":81,"rx":58,"fill":166,"stroke":167,"style":84},"324","60","412",[68,1038,1041],{"x":1039,"y":1040,"style":89,"fill":167,"textAnchor":74},"530","88.805","unattended script",[68,1043,1045],{"x":1039,"y":1044,"style":94,"fill":83,"textAnchor":74},"108.755","log errors · rollBack() · exit non-zero",[44,1047],{"x":1034,"y":1048,"width":1036,"height":81,"rx":58,"fill":115,"stroke":116,"style":84},"146",[68,1050,1052],{"x":1039,"y":1051,"style":89,"fill":116,"textAnchor":74},"174.805","interactive plugin",[68,1054,1056],{"x":1039,"y":1055,"style":94,"fill":83,"textAnchor":74},"194.755","show errors · keep editing · user fixes and saves",[228,1058,1060],{"className":230,"code":1059,"language":232,"meta":233,"style":233},"from qgis.core import Qgis, QgsMessageLog\n\ndef commit_or_report(layer, interactive, iface=None):\n    if layer.commitChanges():\n        return True\n    errors = layer.commitErrors()\n    for line in errors:\n        QgsMessageLog.logMessage(line, \"MyPlugin\", Qgis.Critical)\n    if interactive and iface is not None:\n        iface.messageBar().pushCritical(\"Save failed\", errors[-1] if errors else \"unknown error\")\n        # leave the layer editable so the user can fix and retry\n    else:\n        layer.rollBack()\n    return False\n",[202,1061,1062,1073,1077,1096,1104,1112,1122,1134,1145,1165,1196,1201,1208,1213],{"__ignoreMap":233},[237,1063,1064,1066,1068,1070],{"class":97,"line":239},[237,1065,243],{"class":242},[237,1067,247],{"class":246},[237,1069,250],{"class":242},[237,1071,1072],{"class":246}," Qgis, QgsMessageLog\n",[237,1074,1075],{"class":97,"line":256},[237,1076,260],{"emptyLinePlaceholder":259},[237,1078,1079,1082,1086,1089,1091,1094],{"class":97,"line":263},[237,1080,1081],{"class":242},"def",[237,1083,1085],{"class":1084},"svObZ"," commit_or_report",[237,1087,1088],{"class":246},"(layer, interactive, iface",[237,1090,269],{"class":242},[237,1092,1093],{"class":282},"None",[237,1095,370],{"class":246},[237,1097,1098,1101],{"class":97,"line":288},[237,1099,1100],{"class":242},"    if",[237,1102,1103],{"class":246}," layer.commitChanges():\n",[237,1105,1106,1109],{"class":97,"line":305},[237,1107,1108],{"class":242},"        return",[237,1110,1111],{"class":282}," True\n",[237,1113,1114,1117,1119],{"class":97,"line":310},[237,1115,1116],{"class":246},"    errors ",[237,1118,269],{"class":242},[237,1120,1121],{"class":246}," layer.commitErrors()\n",[237,1123,1124,1126,1129,1131],{"class":97,"line":330},[237,1125,594],{"class":242},[237,1127,1128],{"class":246}," line ",[237,1130,361],{"class":242},[237,1132,1133],{"class":246}," errors:\n",[237,1135,1136,1139,1142],{"class":97,"line":347},[237,1137,1138],{"class":246},"        QgsMessageLog.logMessage(line, ",[237,1140,1141],{"class":275},"\"MyPlugin\"",[237,1143,1144],{"class":246},", Qgis.Critical)\n",[237,1146,1147,1149,1152,1154,1157,1159,1161,1163],{"class":97,"line":352},[237,1148,1100],{"class":242},[237,1150,1151],{"class":246}," interactive ",[237,1153,322],{"class":242},[237,1155,1156],{"class":246}," iface ",[237,1158,733],{"class":242},[237,1160,316],{"class":242},[237,1162,736],{"class":282},[237,1164,589],{"class":246},[237,1166,1167,1170,1173,1176,1178,1181,1184,1186,1189,1191,1194],{"class":97,"line":373},[237,1168,1169],{"class":246},"        iface.messageBar().pushCritical(",[237,1171,1172],{"class":275},"\"Save failed\"",[237,1174,1175],{"class":246},", errors[",[237,1177,679],{"class":242},[237,1179,1180],{"class":282},"1",[237,1182,1183],{"class":246},"] ",[237,1185,313],{"class":242},[237,1187,1188],{"class":246}," errors ",[237,1190,642],{"class":242},[237,1192,1193],{"class":275}," \"unknown error\"",[237,1195,302],{"class":246},[237,1197,1198],{"class":97,"line":384},[237,1199,1200],{"class":631},"        # leave the layer editable so the user can fix and retry\n",[237,1202,1203,1206],{"class":97,"line":389},[237,1204,1205],{"class":242},"    else",[237,1207,589],{"class":246},[237,1209,1210],{"class":97,"line":403},[237,1211,1212],{"class":246},"        layer.rollBack()\n",[237,1214,1215,1218],{"class":97,"line":408},[237,1216,1217],{"class":242},"    return",[237,1219,1220],{"class":282}," False\n",[14,1222,1223,1225,1226,1228,1229,1233],{},[444,1224,446],{}," The last line of ",[202,1227,972],{}," is usually the provider's own message — the database error text — and is the most useful one to show. In a plugin, leaving the layer in edit mode after a failure respects the user's work: their other edits are still in the buffer. In an unattended script, rolling back keeps the data source and the project in a known state, and the logged errors explain why the run made no changes. ",[21,1230,1232],{"href":1231},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Flog-messages-to-qgis-message-log-pyqgis\u002F","Logging messages to the QGIS message log"," covers channels and levels.",[187,1235,1237],{"id":1236},"edit-several-layers-together","Edit several layers together",[14,1239,1240],{},"Changes that span layers — a parcel split that also updates an owner table — should succeed or fail together. With transaction groups enabled on the project, layers from the same database share one transaction, and committing one commits all.",[228,1242,1244],{"className":230,"code":1243,"language":232,"meta":233,"style":233},"from qgis.core import Qgis, QgsProject\n\nproject = QgsProject.instance()\nproject.setTransactionMode(Qgis.TransactionMode.AutomaticGroups)\n# reload layers or reopen the project for the mode to apply to them\n\nparcels = project.mapLayersByName(\"parcels\")[0]\nowners = project.mapLayersByName(\"owners\")[0]\nparcels.startEditing()                 # also puts owners into edit mode\n# ... changes to both layers ...\nparcels.commitChanges()                # one database COMMIT for both\n",[202,1245,1246,1257,1261,1271,1276,1281,1285,1304,1322,1330,1335],{"__ignoreMap":233},[237,1247,1248,1250,1252,1254],{"class":97,"line":239},[237,1249,243],{"class":242},[237,1251,247],{"class":246},[237,1253,250],{"class":242},[237,1255,1256],{"class":246}," Qgis, QgsProject\n",[237,1258,1259],{"class":97,"line":256},[237,1260,260],{"emptyLinePlaceholder":259},[237,1262,1263,1266,1268],{"class":97,"line":263},[237,1264,1265],{"class":246},"project ",[237,1267,269],{"class":242},[237,1269,1270],{"class":246}," QgsProject.instance()\n",[237,1272,1273],{"class":97,"line":288},[237,1274,1275],{"class":246},"project.setTransactionMode(Qgis.TransactionMode.AutomaticGroups)\n",[237,1277,1278],{"class":97,"line":305},[237,1279,1280],{"class":631},"# reload layers or reopen the project for the mode to apply to them\n",[237,1282,1283],{"class":97,"line":310},[237,1284,260],{"emptyLinePlaceholder":259},[237,1286,1287,1290,1292,1295,1298,1300,1302],{"class":97,"line":330},[237,1288,1289],{"class":246},"parcels ",[237,1291,269],{"class":242},[237,1293,1294],{"class":246}," project.mapLayersByName(",[237,1296,1297],{"class":275},"\"parcels\"",[237,1299,279],{"class":246},[237,1301,46],{"class":282},[237,1303,285],{"class":246},[237,1305,1306,1309,1311,1313,1316,1318,1320],{"class":97,"line":347},[237,1307,1308],{"class":246},"owners ",[237,1310,269],{"class":242},[237,1312,1294],{"class":246},[237,1314,1315],{"class":275},"\"owners\"",[237,1317,279],{"class":246},[237,1319,46],{"class":282},[237,1321,285],{"class":246},[237,1323,1324,1327],{"class":97,"line":352},[237,1325,1326],{"class":246},"parcels.startEditing()                 ",[237,1328,1329],{"class":631},"# also puts owners into edit mode\n",[237,1331,1332],{"class":97,"line":373},[237,1333,1334],{"class":631},"# ... changes to both layers ...\n",[237,1336,1337,1340],{"class":97,"line":384},[237,1338,1339],{"class":246},"parcels.commitChanges()                ",[237,1341,1342],{"class":631},"# one database COMMIT for both\n",[14,1344,1345,1347,1348,474],{},[444,1346,446],{}," In automatic transaction groups, layers from the same PostgreSQL or GeoPackage connection join a single database transaction when editing starts, and edits are sent to the database immediately inside that transaction rather than held in the buffer. Commit and rollback apply to the whole group. The mode is a project property and takes effect for layers loaded after it is set. The trade-offs, including locking, are covered in ",[21,1349,1351],{"href":1350},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis\u002F","editing features with transactions",[187,1353,1355],{"id":1354},"react-to-edits-with-layer-signals","React to edits with layer signals",[14,1357,1358],{},"Plugins often need to respond to edits rather than make them: recompute a derived field when a geometry changes, refuse a commit that breaks a business rule, or refresh a dock widget after a save. The layer emits signals at each stage of the buffer's life, and connecting to them is how a plugin stays in step with the user's editing.",[228,1360,1362],{"className":230,"code":1361,"language":232,"meta":233,"style":233},"def on_geometry_changed(fid, geom):\n    length_idx = pipes.fields().indexOf(\"length_m\")\n    pipes.changeAttributeValue(fid, length_idx, round(geom.length(), 2))\n\ndef on_before_commit():\n    buf = pipes.editBuffer()\n    for fid, f in buf.addedFeatures().items():\n        if not f[\"material\"]:\n            iface.messageBar().pushWarning(\"Pipes\", \"New pipe without material\")\n\ndef on_after_commit():\n    print(\"saved; refreshing summary panel\")\n\npipes.geometryChanged.connect(on_geometry_changed)\npipes.beforeCommitChanges.connect(on_before_commit)\npipes.afterCommitChanges.connect(on_after_commit)\n",[202,1363,1364,1374,1388,1404,1408,1418,1427,1439,1454,1469,1473,1482,1493,1497,1502,1507],{"__ignoreMap":233},[237,1365,1366,1368,1371],{"class":97,"line":239},[237,1367,1081],{"class":242},[237,1369,1370],{"class":1084}," on_geometry_changed",[237,1372,1373],{"class":246},"(fid, geom):\n",[237,1375,1376,1379,1381,1383,1386],{"class":97,"line":256},[237,1377,1378],{"class":246},"    length_idx ",[237,1380,269],{"class":242},[237,1382,296],{"class":246},[237,1384,1385],{"class":275},"\"length_m\"",[237,1387,302],{"class":246},[237,1389,1390,1393,1396,1399,1402],{"class":97,"line":263},[237,1391,1392],{"class":246},"    pipes.changeAttributeValue(fid, length_idx, ",[237,1394,1395],{"class":282},"round",[237,1397,1398],{"class":246},"(geom.length(), ",[237,1400,1401],{"class":282},"2",[237,1403,685],{"class":246},[237,1405,1406],{"class":97,"line":288},[237,1407,260],{"emptyLinePlaceholder":259},[237,1409,1410,1412,1415],{"class":97,"line":305},[237,1411,1081],{"class":242},[237,1413,1414],{"class":1084}," on_before_commit",[237,1416,1417],{"class":246},"():\n",[237,1419,1420,1423,1425],{"class":97,"line":310},[237,1421,1422],{"class":246},"    buf ",[237,1424,269],{"class":242},[237,1426,723],{"class":246},[237,1428,1429,1431,1434,1436],{"class":97,"line":330},[237,1430,594],{"class":242},[237,1432,1433],{"class":246}," fid, f ",[237,1435,361],{"class":242},[237,1437,1438],{"class":246}," buf.addedFeatures().items():\n",[237,1440,1441,1444,1446,1449,1452],{"class":97,"line":347},[237,1442,1443],{"class":242},"        if",[237,1445,316],{"class":242},[237,1447,1448],{"class":246}," f[",[237,1450,1451],{"class":275},"\"material\"",[237,1453,893],{"class":246},[237,1455,1456,1459,1462,1464,1467],{"class":97,"line":352},[237,1457,1458],{"class":246},"            iface.messageBar().pushWarning(",[237,1460,1461],{"class":275},"\"Pipes\"",[237,1463,209],{"class":246},[237,1465,1466],{"class":275},"\"New pipe without material\"",[237,1468,302],{"class":246},[237,1470,1471],{"class":97,"line":373},[237,1472,260],{"emptyLinePlaceholder":259},[237,1474,1475,1477,1480],{"class":97,"line":384},[237,1476,1081],{"class":242},[237,1478,1479],{"class":1084}," on_after_commit",[237,1481,1417],{"class":246},[237,1483,1484,1486,1488,1491],{"class":97,"line":389},[237,1485,421],{"class":282},[237,1487,339],{"class":246},[237,1489,1490],{"class":275},"\"saved; refreshing summary panel\"",[237,1492,302],{"class":246},[237,1494,1495],{"class":97,"line":403},[237,1496,260],{"emptyLinePlaceholder":259},[237,1498,1499],{"class":97,"line":408},[237,1500,1501],{"class":246},"pipes.geometryChanged.connect(on_geometry_changed)\n",[237,1503,1504],{"class":97,"line":418},[237,1505,1506],{"class":246},"pipes.beforeCommitChanges.connect(on_before_commit)\n",[237,1508,1509],{"class":97,"line":437},[237,1510,1511],{"class":246},"pipes.afterCommitChanges.connect(on_after_commit)\n",[14,1513,1514,690,1516,1519,1520,1523,1524,1527,1528,1531,1532,1535,1536,1540],{},[444,1515,446],{},[202,1517,1518],{},"geometryChanged"," fires for every reshaped feature while editing, so the derived ",[202,1521,1522],{},"length_m"," stays current inside the same edit session — and it goes onto the undo stack together with the user's own change when the edit happens inside a tool's edit command. ",[202,1525,1526],{},"beforeCommitChanges"," is the last moment to inspect the buffer; it cannot veto the commit by itself, so warn here or validate with field constraints instead of relying on it as a gate. ",[202,1529,1530],{},"afterCommitChanges"," fires only on a successful save, which makes it the right hook for refreshing anything that reads from the data source. Disconnect these handlers in the plugin's ",[202,1533,1534],{},"unload()",", as described in ",[21,1537,1539],{"href":1538},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","connecting layer signals",", or they keep firing after the plugin is reloaded.",[187,1542,1544],{"id":1543},"qgis-version-compatibility","QGIS version compatibility",[14,1546,1547,1548,209,1551,209,1554,1557,1558,1560,1561,1564,1565,1568,1569,1572],{},"The edit buffer API — ",[202,1549,1550],{},"startEditing",[202,1552,1553],{},"commitChanges",[202,1555,1556],{},"rollBack",", edit commands and ",[202,1559,948],{}," accessors — is unchanged from QGIS 3.0 to QGIS 4. ",[202,1562,1563],{},"Qgis.TransactionMode"," replaced the boolean ",[202,1566,1567],{},"setAutoTransaction"," in 3.26; use the enum on all current releases. ",[202,1570,1571],{},"commitChanges(stopEditing=False)"," keeps the layer in edit mode after a successful save, available since 3.10.",[187,1574,1576],{"id":1575},"troubleshooting","Troubleshooting",[192,1578,1579,1590,1596,1602],{},[195,1580,1581,1586,1587,1589],{},[444,1582,1583,1585],{},[202,1584,454],{}," returns False."," The layer is already editable, or the provider is read-only; check ",[202,1588,450],{}," and the capabilities.",[195,1591,1592,1595],{},[444,1593,1594],{},"Changes appear in the table but not in the file."," The layer is still in edit mode; nothing is written until commit.",[195,1597,1598,1601],{},[444,1599,1600],{},"Undo removes only one feature of my batch."," The changes were not wrapped in an edit command.",[195,1603,1604,1607,1608,474],{},[444,1605,1606],{},"QGIS crashes when a background task edits."," The edit buffer must only be touched on the main thread; return results from the task and apply them in ",[202,1609,1610],{},"finished()",[187,1612,1614],{"id":1613},"conclusion","Conclusion",[14,1616,1617,1618,1621,1622,1624],{},"Respect an existing edit session, wrap a script's changes in a named edit command so one undo reverses them, use ",[202,1619,1620],{},"destroyEditCommand"," to back out a failed batch, read the buffer to summarise or validate pending changes, and treat ",[202,1623,972],{}," as the explanation to log or show when a save fails.",[187,1626,1628],{"id":1627},"frequently-asked-questions","Frequently Asked Questions",[14,1630,1631,1634],{},[444,1632,1633],{},"Does writing through the data provider use the undo stack?","\nNo. Provider writes bypass the buffer entirely and cannot be undone from QGIS.",[14,1636,1637,1640,1641,474],{},[444,1638,1639],{},"Can I commit without leaving edit mode?","\nYes: ",[202,1642,1643],{},"layer.commitChanges(stopEditing=False)",[14,1645,1646,1649,1652],{},[444,1647,1648],{},"How do I trigger undo from code?",[202,1650,1651],{},"layer.undoStack().undo()",", the same action as Ctrl+Z.",[14,1654,1655,1658],{},[444,1656,1657],{},"Are memory layers edited the same way?","\nYes. The buffer works identically; committing writes into the memory provider.",[187,1660,1662],{"id":1661},"related","Related",[192,1664,1665,1670,1675,1680,1686],{},[195,1666,1667,1669],{},[21,1668,24],{"href":23}," — the guide this recipe belongs to",[195,1671,1672],{},[21,1673,1674],{"href":472},"Add Features to a Vector Layer in PyQGIS",[195,1676,1677],{},[21,1678,1679],{"href":1350},"Edit Features with Transactions in PyQGIS",[195,1681,1682],{},[21,1683,1685],{"href":1684},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in PyQGIS",[195,1687,1688],{},[21,1689,1691],{"href":1690},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Background Task with QgsTask",[1693,1694,1695],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":233,"searchDepth":256,"depth":256,"links":1697},[1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709],{"id":189,"depth":256,"text":190},{"id":222,"depth":256,"text":223},{"id":477,"depth":256,"text":478},{"id":704,"depth":256,"text":705},{"id":965,"depth":256,"text":966},{"id":1236,"depth":256,"text":1237},{"id":1354,"depth":256,"text":1355},{"id":1543,"depth":256,"text":1544},{"id":1575,"depth":256,"text":1576},{"id":1613,"depth":256,"text":1614},{"id":1627,"depth":256,"text":1628},{"id":1661,"depth":256,"text":1662},"Use a vector layer's edit buffer from Python — start and commit edit sessions, group changes into single undo steps with edit commands, inspect pending changes before saving, roll back cleanly and read commitErrors when a save fails.","md",{"slug":1713,"type":1714,"breadcrumb":1715,"datePublished":1716,"dateModified":1716},"undo-edits-with-edit-buffer-pyqgis","article","Undo Edits with the Edit Buffer","2026-10-02","\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fundo-edits-with-edit-buffer-pyqgis",{"title":5,"description":1710},"pyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fundo-edits-with-edit-buffer-pyqgis\u002Findex","gvGE8g33O4YIIzh_f_4P7ZxvPSDxw7pbDnA8rtt_iZs",1790966252949]