[{"data":1,"prerenderedAt":1959},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-geometry-validity-report-pyqgis":3},{"id":4,"title":5,"body":6,"description":1948,"extension":1949,"meta":1950,"navigation":248,"path":1955,"seo":1956,"stem":1957,"__hash__":1958},"docs\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-geometry-validity-report-pyqgis\u002Findex.md","Build a Geometry Validity Report in PyQGIS",{"type":7,"value":8,"toc":1934},"minimark",[9,13,17,31,178,183,200,204,207,502,525,529,536,605,612,619,623,626,963,968,972,975,1046,1227,1243,1247,1250,1520,1534,1538,1541,1781,1790,1794,1812,1816,1852,1856,1862,1866,1872,1878,1888,1898,1902,1930],[10,11,5],"h1",{"id":12},"build-a-geometry-validity-report-in-pyqgis",[14,15,16],"p",{},"Invalid geometry is the most common hidden defect in vector data. A polygon whose boundary crosses itself still draws on the map, still has an attribute row and still has an area — but buffers, intersections, dissolves and spatial joins computed from it can be wrong or fail outright. Repairing it is easy; knowing what was wrong, where, and how much of the layer is affected is what lets you decide whether a repair is safe or whether the data needs to go back to whoever produced it.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002F","Data Quality & Topology Validation",". It runs the Check Validity algorithm, explains the two rule sets it offers, turns its output into a summary by error type, and records the result as a reusable report. Repair itself is covered in ",[21,27,29],{"href":28},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Ffix-invalid-geometries-pyqgis\u002F","fixing invalid geometries",".",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,101,108,115,119,122,128,130,134,142,148,152,157,160,163,166,170,174],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 280","img","The Check Validity algorithm splits a layer into valid features, invalid features with an error message, and one error point per problem","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Three outputs from one check",[45,46,47],"desc",{},"The Check Validity algorithm reads the input layer and splits it into three outputs: valid features, unchanged; invalid features, with an extra field holding the error message; and an error point layer with one point per detected problem located where the problem is, for example at a self-intersection. The report is built from the invalid features and error points.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","280","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"valOutArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","One run, three layers",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"24","104","180","80","#fffdf7","#59645f","stroke-width:2",[73,91,95],{"x":92,"y":93,"style":94,"fill":78,"textAnchor":79},"114","137.805","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","input layer",[73,97,100],{"x":92,"y":98,"style":99,"fill":88,"textAnchor":79},"157.755","text-anchor:middle;font-size:10.5px;font-family:sans-serif","parcels.gpkg",[102,103],"line",{"x1":104,"y1":105,"x2":106,"y2":105,"stroke":71,"style":107},"204","144","250","stroke-width:1.8;marker-end:url(#valOutArrow)",[49,109],{"x":110,"y":111,"width":112,"height":84,"rx":63,"fill":113,"stroke":114,"style":89},"258","92","190","#eef7f4","#0f766e",[73,116,118],{"x":117,"y":93,"style":94,"fill":114,"textAnchor":79},"353","qgis:checkvalidity",[73,120,121],{"x":117,"y":98,"style":99,"fill":88,"textAnchor":79},"METHOD: QGIS or GEOS",[102,123],{"x1":124,"y1":125,"x2":126,"y2":127,"stroke":71,"style":107},"448","120","500","70",[102,129],{"x1":124,"y1":105,"x2":126,"y2":105,"stroke":71,"style":107},[102,131],{"x1":124,"y1":132,"x2":126,"y2":133,"stroke":71,"style":107},"168","218",[49,135],{"x":136,"y":137,"width":138,"height":139,"rx":63,"fill":140,"stroke":141,"style":89},"508","44","228","54","#e8efe6","#15803d",[73,143,147],{"x":144,"y":145,"style":94,"fill":146,"textAnchor":79},"622","64.805","#166534","VALID_OUTPUT",[73,149,151],{"x":144,"y":150,"style":99,"fill":88,"textAnchor":79},"84.755","unchanged features",[49,153],{"x":136,"y":154,"width":138,"height":139,"rx":63,"fill":155,"stroke":156,"style":89},"117","#fdf2e2","#b45309",[73,158,159],{"x":144,"y":93,"style":94,"fill":156,"textAnchor":79},"INVALID_OUTPUT",[73,161,162],{"x":144,"y":98,"style":99,"fill":88,"textAnchor":79},"+ _errors field",[49,164],{"x":136,"y":112,"width":138,"height":139,"rx":63,"fill":155,"stroke":165,"style":89},"#b91c1c",[73,167,169],{"x":144,"y":168,"style":94,"fill":165,"textAnchor":79},"210.805","ERROR_OUTPUT",[73,171,173],{"x":144,"y":172,"style":99,"fill":88,"textAnchor":79},"230.755","a point per problem",[73,175,177],{"x":75,"y":176,"style":99,"fill":88,"textAnchor":79},"266","the report is built from the last two",[179,180,182],"h2",{"id":181},"prerequisites","Prerequisites",[184,185,186,190,193],"ul",{},[187,188,189],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with the Processing plugin enabled (it is by default).",[187,191,192],{},"A polygon or line layer to check. Point layers can only be invalid in trivial ways — an empty geometry or a NaN coordinate — so validity checks matter most for polygons.",[187,194,195,196,30],{},"Standalone scripts need Processing initialised, as described in ",[21,197,199],{"href":198},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Frunning-python-scripts-outside-qgis-desktop\u002F","running Python scripts outside QGIS Desktop",[179,201,203],{"id":202},"run-the-check","Run the check",[14,205,206],{},"The algorithm takes the input layer, a method, and three outputs. Writing the outputs to memory keeps a first look fast; the report section below writes them to a GeoPackage.",[208,209,214],"pre",{"className":210,"code":211,"language":212,"meta":213,"style":213},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsProject\n\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\n\nresult = processing.run(\"qgis:checkvalidity\", {\n    \"INPUT_LAYER\": parcels,\n    \"METHOD\": 2,                       # 1 = QGIS rules, 2 = GEOS rules\n    \"IGNORE_RING_SELF_INTERSECTION\": False,\n    \"VALID_OUTPUT\": \"memory:valid\",\n    \"INVALID_OUTPUT\": \"memory:invalid\",\n    \"ERROR_OUTPUT\": \"memory:errors\",\n})\n\nvalid, invalid, errors = (result[\"VALID_OUTPUT\"], result[\"INVALID_OUTPUT\"],\n                          result[\"ERROR_OUTPUT\"])\nprint(f\"{result['VALID_COUNT']} valid, {result['INVALID_COUNT']} invalid, \"\n      f\"{result['ERROR_COUNT']} problems\")\nQgsProject.instance().addMapLayers([invalid, errors])\n","python","",[215,216,217,229,243,250,275,280,297,306,325,339,352,365,378,384,389,412,424,471,496],"code",{"__ignoreMap":213},[218,219,221,225],"span",{"class":102,"line":220},1,[218,222,224],{"class":223},"snl16","import",[218,226,228],{"class":227},"s95oV"," processing\n",[218,230,232,235,238,240],{"class":102,"line":231},2,[218,233,234],{"class":223},"from",[218,236,237],{"class":227}," qgis.core ",[218,239,224],{"class":223},[218,241,242],{"class":227}," QgsProject\n",[218,244,246],{"class":102,"line":245},3,[218,247,249],{"emptyLinePlaceholder":248},true,"\n",[218,251,253,256,259,262,266,269,272],{"class":102,"line":252},4,[218,254,255],{"class":227},"parcels ",[218,257,258],{"class":223},"=",[218,260,261],{"class":227}," QgsProject.instance().mapLayersByName(",[218,263,265],{"class":264},"sU2Wk","\"parcels\"",[218,267,268],{"class":227},")[",[218,270,51],{"class":271},"sDLfK",[218,273,274],{"class":227},"]\n",[218,276,278],{"class":102,"line":277},5,[218,279,249],{"emptyLinePlaceholder":248},[218,281,283,286,288,291,294],{"class":102,"line":282},6,[218,284,285],{"class":227},"result ",[218,287,258],{"class":223},[218,289,290],{"class":227}," processing.run(",[218,292,293],{"class":264},"\"qgis:checkvalidity\"",[218,295,296],{"class":227},", {\n",[218,298,300,303],{"class":102,"line":299},7,[218,301,302],{"class":264},"    \"INPUT_LAYER\"",[218,304,305],{"class":227},": parcels,\n",[218,307,309,312,315,318,321],{"class":102,"line":308},8,[218,310,311],{"class":264},"    \"METHOD\"",[218,313,314],{"class":227},": ",[218,316,317],{"class":271},"2",[218,319,320],{"class":227},",                       ",[218,322,324],{"class":323},"sjoCn","# 1 = QGIS rules, 2 = GEOS rules\n",[218,326,328,331,333,336],{"class":102,"line":327},9,[218,329,330],{"class":264},"    \"IGNORE_RING_SELF_INTERSECTION\"",[218,332,314],{"class":227},[218,334,335],{"class":271},"False",[218,337,338],{"class":227},",\n",[218,340,342,345,347,350],{"class":102,"line":341},10,[218,343,344],{"class":264},"    \"VALID_OUTPUT\"",[218,346,314],{"class":227},[218,348,349],{"class":264},"\"memory:valid\"",[218,351,338],{"class":227},[218,353,355,358,360,363],{"class":102,"line":354},11,[218,356,357],{"class":264},"    \"INVALID_OUTPUT\"",[218,359,314],{"class":227},[218,361,362],{"class":264},"\"memory:invalid\"",[218,364,338],{"class":227},[218,366,368,371,373,376],{"class":102,"line":367},12,[218,369,370],{"class":264},"    \"ERROR_OUTPUT\"",[218,372,314],{"class":227},[218,374,375],{"class":264},"\"memory:errors\"",[218,377,338],{"class":227},[218,379,381],{"class":102,"line":380},13,[218,382,383],{"class":227},"})\n",[218,385,387],{"class":102,"line":386},14,[218,388,249],{"emptyLinePlaceholder":248},[218,390,392,395,397,400,403,406,409],{"class":102,"line":391},15,[218,393,394],{"class":227},"valid, invalid, errors ",[218,396,258],{"class":223},[218,398,399],{"class":227}," (result[",[218,401,402],{"class":264},"\"VALID_OUTPUT\"",[218,404,405],{"class":227},"], result[",[218,407,408],{"class":264},"\"INVALID_OUTPUT\"",[218,410,411],{"class":227},"],\n",[218,413,415,418,421],{"class":102,"line":414},16,[218,416,417],{"class":227},"                          result[",[218,419,420],{"class":264},"\"ERROR_OUTPUT\"",[218,422,423],{"class":227},"])\n",[218,425,427,430,433,436,439,442,445,448,451,454,457,459,461,464,466,468],{"class":102,"line":426},17,[218,428,429],{"class":271},"print",[218,431,432],{"class":227},"(",[218,434,435],{"class":223},"f",[218,437,438],{"class":264},"\"",[218,440,441],{"class":271},"{",[218,443,444],{"class":227},"result[",[218,446,447],{"class":264},"'VALID_COUNT'",[218,449,450],{"class":227},"]",[218,452,453],{"class":271},"}",[218,455,456],{"class":264}," valid, ",[218,458,441],{"class":271},[218,460,444],{"class":227},[218,462,463],{"class":264},"'INVALID_COUNT'",[218,465,450],{"class":227},[218,467,453],{"class":271},[218,469,470],{"class":264}," invalid, \"\n",[218,472,474,477,479,481,483,486,488,490,493],{"class":102,"line":473},18,[218,475,476],{"class":223},"      f",[218,478,438],{"class":264},[218,480,441],{"class":271},[218,482,444],{"class":227},[218,484,485],{"class":264},"'ERROR_COUNT'",[218,487,450],{"class":227},[218,489,453],{"class":271},[218,491,492],{"class":264}," problems\"",[218,494,495],{"class":227},")\n",[218,497,499],{"class":102,"line":498},19,[218,500,501],{"class":227},"QgsProject.instance().addMapLayers([invalid, errors])\n",[14,503,504,508,509,512,513,516,517,520,521,524],{},[505,506,507],"strong",{},"Breakdown:"," The three count outputs give the headline numbers without iterating anything. ",[215,510,511],{},"INVALID_COUNT"," counts features; ",[215,514,515],{},"ERROR_COUNT"," counts problems, and a single badly digitized parcel can contribute several. The invalid layer is a copy of the offending features with an ",[215,518,519],{},"_errors"," field added; the errors layer holds points with a ",[215,522,523],{},"message"," field, placed where each problem is. Adding both to the map makes inspection quick: style the error points boldly and zoom through them.",[179,526,528],{"id":527},"choose-the-rule-set-deliberately","Choose the rule set deliberately",[14,530,531,532,535],{},"The ",[215,533,534],{},"METHOD"," parameter picks between two validators, and they do not agree on everything. The difference matters when you decide what \"valid\" means for a dataset.",[14,537,538],{},[34,539,542,545,548,551,554,561,566,570,574,578,582,585,589,592,595,598,601],{"viewBox":540,"role":37,"ariaLabel":541,"xmlns":39},"0 0 760 270","Comparison of the GEOS and QGIS validation methods, with what each reports and when to use it",[41,543,544],{},"QGIS rules and GEOS rules",[45,546,547],{},"The GEOS method applies the OGC simple features rules used by GEOS-based operations, PostGIS ST_IsValid and most other tools; it is strict about rings touching themselves at a point and reports one problem per feature. The QGIS method uses QGIS's own validator, reports every problem it finds, and can be told to ignore ring self-intersections, which some data models such as ESRI's allow. The default setting follows the Digitizing options.",[49,549],{"x":51,"y":51,"width":52,"height":550,"fill":54},"270",[73,552,553],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Same polygon, two opinions",[49,555],{"x":83,"y":556,"width":557,"height":558,"rx":63,"fill":559,"stroke":560,"style":89},"56","340","170","#eff3ff","#2563eb",[73,562,565],{"x":563,"y":564,"style":94,"fill":560,"textAnchor":79},"194","92.78","GEOS (METHOD 2)",[73,567,569],{"x":563,"y":568,"style":99,"fill":71,"textAnchor":79},"118.78","OGC simple features rules",[73,571,573],{"x":563,"y":572,"style":99,"fill":71,"textAnchor":79},"144.78","matches PostGIS ST_IsValid",[73,575,577],{"x":563,"y":576,"style":99,"fill":71,"textAnchor":79},"170.78","first problem per feature",[73,579,581],{"x":563,"y":580,"style":99,"fill":88,"textAnchor":79},"196.78","use when data goes to GEOS tools",[49,583],{"x":584,"y":556,"width":557,"height":558,"rx":63,"fill":155,"stroke":156,"style":89},"396",[73,586,588],{"x":587,"y":564,"style":94,"fill":156,"textAnchor":79},"566","QGIS (METHOD 1)",[73,590,591],{"x":587,"y":568,"style":99,"fill":71,"textAnchor":79},"QGIS's own validator",[73,593,594],{"x":587,"y":572,"style":99,"fill":71,"textAnchor":79},"every problem it finds",[73,596,597],{"x":587,"y":576,"style":99,"fill":71,"textAnchor":79},"can ignore ring self-touch",[73,599,600],{"x":587,"y":580,"style":99,"fill":88,"textAnchor":79},"use to list all issues",[73,602,604],{"x":75,"y":603,"style":99,"fill":88,"textAnchor":79},"252","METHOD 0 follows the Digitizing settings — avoid it in scripts",[14,606,607,608,611],{},"The GEOS method answers the question \"will GEOS-based operations behave correctly on this geometry?\" — which is what matters before a buffer, an overlay or a load into PostGIS, because they all use the same rules. It stops at the first problem in each feature. The QGIS method lists every problem it can find, which is more useful when someone has to fix the data by hand. ",[215,609,610],{},"IGNORE_RING_SELF_INTERSECTION"," relaxes one specific rule — a ring touching itself at a single point — that some data models consider legitimate; turning it on with the GEOS method makes the result disagree with GEOS-based operations, so use it only with the QGIS method and only when you know the downstream tools accept such rings.",[14,613,614,615,618],{},"Avoid ",[215,616,617],{},"METHOD: 0"," in scripts. It means \"whatever the user's Digitizing settings say\", so the same script gives different results on different machines.",[179,620,622],{"id":621},"summarise-errors-by-reason","Summarise errors by reason",[14,624,625],{},"A list of four thousand error points is hard to act on. Grouping them by message turns it into a short table: how many self-intersections, how many duplicate nodes, how many rings with too few points.",[208,627,629],{"className":210,"code":628,"language":212,"meta":213,"style":213},"import re\nfrom collections import Counter\n\ndef reason(message):\n    \"\"\"Strip coordinates so equal problems group together.\"\"\"\n    text = re.sub(r\"\\[?-?\\d+(\\.\\d+)?[ ,]+-?\\d+(\\.\\d+)?\\]?\", \"\", message)\n    return re.sub(r\"\\s+\", \" \", text).strip(\" :.\")\n\ncounts = Counter(reason(f[\"message\"]) for f in errors.getFeatures())\nfor why, n in counts.most_common():\n    print(f\"{n:>6}  {why}\")\n\nworst = sorted(invalid.getFeatures(), key=lambda f: len(str(f[\"_errors\"])), reverse=True)[:5]\nfor f in worst:\n    print(f[\"parcel_id\"], str(f[\"_errors\"])[:80])\n",[215,630,631,638,650,654,666,671,760,791,795,823,835,868,872,927,938],{"__ignoreMap":213},[218,632,633,635],{"class":102,"line":220},[218,634,224],{"class":223},[218,636,637],{"class":227}," re\n",[218,639,640,642,645,647],{"class":102,"line":231},[218,641,234],{"class":223},[218,643,644],{"class":227}," collections ",[218,646,224],{"class":223},[218,648,649],{"class":227}," Counter\n",[218,651,652],{"class":102,"line":245},[218,653,249],{"emptyLinePlaceholder":248},[218,655,656,659,663],{"class":102,"line":252},[218,657,658],{"class":223},"def",[218,660,662],{"class":661},"svObZ"," reason",[218,664,665],{"class":227},"(message):\n",[218,667,668],{"class":102,"line":277},[218,669,670],{"class":264},"    \"\"\"Strip coordinates so equal problems group together.\"\"\"\n",[218,672,673,676,678,681,684,686,690,693,697,699,702,705,707,710,712,714,717,719,722,724,726,728,730,732,734,736,738,740,742,744,747,749,751,754,757],{"class":102,"line":282},[218,674,675],{"class":227},"    text ",[218,677,258],{"class":223},[218,679,680],{"class":227}," re.sub(",[218,682,683],{"class":223},"r",[218,685,438],{"class":264},[218,687,689],{"class":688},"sRjNt","\\[",[218,691,692],{"class":223},"?",[218,694,696],{"class":695},"sns5M","-",[218,698,692],{"class":223},[218,700,701],{"class":271},"\\d",[218,703,704],{"class":223},"+",[218,706,432],{"class":271},[218,708,709],{"class":688},"\\.",[218,711,701],{"class":271},[218,713,704],{"class":223},[218,715,716],{"class":271},")",[218,718,692],{"class":223},[218,720,721],{"class":271},"[ ,]",[218,723,704],{"class":223},[218,725,696],{"class":695},[218,727,692],{"class":223},[218,729,701],{"class":271},[218,731,704],{"class":223},[218,733,432],{"class":271},[218,735,709],{"class":688},[218,737,701],{"class":271},[218,739,704],{"class":223},[218,741,716],{"class":271},[218,743,692],{"class":223},[218,745,746],{"class":688},"\\]",[218,748,692],{"class":223},[218,750,438],{"class":264},[218,752,753],{"class":227},", ",[218,755,756],{"class":264},"\"\"",[218,758,759],{"class":227},", message)\n",[218,761,762,765,767,769,771,774,776,778,780,783,786,789],{"class":102,"line":299},[218,763,764],{"class":223},"    return",[218,766,680],{"class":227},[218,768,683],{"class":223},[218,770,438],{"class":264},[218,772,773],{"class":271},"\\s",[218,775,704],{"class":223},[218,777,438],{"class":264},[218,779,753],{"class":227},[218,781,782],{"class":264},"\" \"",[218,784,785],{"class":227},", text).strip(",[218,787,788],{"class":264},"\" :.\"",[218,790,495],{"class":227},[218,792,793],{"class":102,"line":308},[218,794,249],{"emptyLinePlaceholder":248},[218,796,797,800,802,805,808,811,814,817,820],{"class":102,"line":327},[218,798,799],{"class":227},"counts ",[218,801,258],{"class":223},[218,803,804],{"class":227}," Counter(reason(f[",[218,806,807],{"class":264},"\"message\"",[218,809,810],{"class":227},"]) ",[218,812,813],{"class":223},"for",[218,815,816],{"class":227}," f ",[218,818,819],{"class":223},"in",[218,821,822],{"class":227}," errors.getFeatures())\n",[218,824,825,827,830,832],{"class":102,"line":341},[218,826,813],{"class":223},[218,828,829],{"class":227}," why, n ",[218,831,819],{"class":223},[218,833,834],{"class":227}," counts.most_common():\n",[218,836,837,840,842,844,846,848,851,854,856,859,862,864,866],{"class":102,"line":354},[218,838,839],{"class":271},"    print",[218,841,432],{"class":227},[218,843,435],{"class":223},[218,845,438],{"class":264},[218,847,441],{"class":271},[218,849,850],{"class":227},"n",[218,852,853],{"class":223},":>6",[218,855,453],{"class":271},[218,857,858],{"class":271},"  {",[218,860,861],{"class":227},"why",[218,863,453],{"class":271},[218,865,438],{"class":264},[218,867,495],{"class":227},[218,869,870],{"class":102,"line":367},[218,871,249],{"emptyLinePlaceholder":248},[218,873,874,877,879,882,885,889,892,895,898,900,903,906,909,912,915,917,920,923,925],{"class":102,"line":380},[218,875,876],{"class":227},"worst ",[218,878,258],{"class":223},[218,880,881],{"class":271}," sorted",[218,883,884],{"class":227},"(invalid.getFeatures(), ",[218,886,888],{"class":887},"s9osk","key",[218,890,891],{"class":223},"=lambda",[218,893,894],{"class":227}," f: ",[218,896,897],{"class":271},"len",[218,899,432],{"class":227},[218,901,902],{"class":271},"str",[218,904,905],{"class":227},"(f[",[218,907,908],{"class":264},"\"_errors\"",[218,910,911],{"class":227},"])), ",[218,913,914],{"class":887},"reverse",[218,916,258],{"class":223},[218,918,919],{"class":271},"True",[218,921,922],{"class":227},")[:",[218,924,64],{"class":271},[218,926,274],{"class":227},[218,928,929,931,933,935],{"class":102,"line":386},[218,930,813],{"class":223},[218,932,816],{"class":227},[218,934,819],{"class":223},[218,936,937],{"class":227}," worst:\n",[218,939,940,942,944,947,950,952,954,956,959,961],{"class":102,"line":391},[218,941,839],{"class":271},[218,943,905],{"class":227},[218,945,946],{"class":264},"\"parcel_id\"",[218,948,949],{"class":227},"], ",[218,951,902],{"class":271},[218,953,905],{"class":227},[218,955,908],{"class":264},[218,957,958],{"class":227},"])[:",[218,960,86],{"class":271},[218,962,423],{"class":227},[14,964,965,967],{},[505,966,507],{}," Error messages include the coordinate of the problem, which would make every message unique; stripping numbers groups them by kind. The most common reasons in practice are self-intersections (a bow-tie or a ring crossing itself), duplicate consecutive nodes, rings with fewer than four points, holes lying outside their shell, and nested shells in a multipolygon. The ranking by error text length is a rough proxy for the most damaged features, which are the ones worth looking at by eye before deciding on an automatic repair.",[179,969,971],{"id":970},"decide-whether-automatic-repair-is-safe","Decide whether automatic repair is safe",[14,973,974],{},"Not every invalid geometry should be repaired automatically. The summary tells you which case you are in.",[14,976,977],{},[34,978,981,984,987,990,993,998,1003,1007,1011,1015,1018,1021,1024,1027,1030,1033,1037,1040,1043],{"viewBox":979,"role":37,"ariaLabel":980,"xmlns":39},"0 0 760 236","A decision guide from the error summary: repair automatically, repair and compare areas, or return the data with the report",[41,982,983],{},"From summary to action",[45,985,986],{},"If the problems are mostly duplicate nodes or tiny self-intersections at a single vertex, an automatic makeValid repair is safe and changes area negligibly. If they are large bow-ties, overlapping shells or holes outside shells, the repair changes the shape meaningfully and the result should be compared with the original by area before use. If a large share of the layer is invalid, the data producer's process is broken and the data should go back with the report.",[49,988],{"x":51,"y":51,"width":52,"height":989,"fill":54},"236",[73,991,992],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"What the summary says to do",[49,994],{"x":83,"y":995,"width":996,"height":997,"rx":63,"fill":140,"stroke":141,"style":89},"60","222","160",[73,999,1002],{"x":1000,"y":1001,"style":94,"fill":146,"textAnchor":79},"135","104.78","small, local",[73,1004,1006],{"x":1000,"y":1005,"style":99,"fill":71,"textAnchor":79},"130.78","duplicate nodes",[73,1008,1010],{"x":1000,"y":1009,"style":99,"fill":71,"textAnchor":79},"156.78","single-vertex touches",[73,1012,1014],{"x":1000,"y":1013,"style":99,"fill":88,"textAnchor":79},"182.78","repair automatically",[49,1016],{"x":1017,"y":995,"width":996,"height":997,"rx":63,"fill":155,"stroke":156,"style":89},"269",[73,1019,1020],{"x":75,"y":1001,"style":94,"fill":156,"textAnchor":79},"shape-changing",[73,1022,1023],{"x":75,"y":1005,"style":99,"fill":71,"textAnchor":79},"bow-ties, nested shells",[73,1025,1026],{"x":75,"y":1009,"style":99,"fill":71,"textAnchor":79},"holes outside shells",[73,1028,1029],{"x":75,"y":1013,"style":99,"fill":88,"textAnchor":79},"repair, compare areas",[49,1031],{"x":1032,"y":995,"width":996,"height":997,"rx":63,"fill":155,"stroke":165,"style":89},"514",[73,1034,1036],{"x":1035,"y":1001,"style":94,"fill":165,"textAnchor":79},"625","widespread",[73,1038,1039],{"x":1035,"y":1005,"style":99,"fill":71,"textAnchor":79},"a large share invalid",[73,1041,1042],{"x":1035,"y":1009,"style":99,"fill":71,"textAnchor":79},"systematic pattern",[73,1044,1045],{"x":1035,"y":1013,"style":99,"fill":88,"textAnchor":79},"send back with report",[208,1047,1049],{"className":210,"code":1048,"language":212,"meta":213,"style":213},"from qgis.core import QgsFeatureRequest\n\nchanged = []\nfor f in parcels.getFeatures(QgsFeatureRequest().setFilterFids(\n        [f[\"fid\"] for f in invalid.getFeatures()])):\n    before = f.geometry()\n    after = before.makeValid()\n    if before.area() > 0:\n        delta = abs(after.area() - before.area()) \u002F before.area()\n        if delta > 0.01:\n            changed.append((f[\"parcel_id\"], round(delta * 100, 2)))\nprint(len(changed), \"features would change area by more than 1 %\")\n",[215,1050,1051,1062,1066,1076,1087,1107,1117,1127,1144,1168,1183,1211],{"__ignoreMap":213},[218,1052,1053,1055,1057,1059],{"class":102,"line":220},[218,1054,234],{"class":223},[218,1056,237],{"class":227},[218,1058,224],{"class":223},[218,1060,1061],{"class":227}," QgsFeatureRequest\n",[218,1063,1064],{"class":102,"line":231},[218,1065,249],{"emptyLinePlaceholder":248},[218,1067,1068,1071,1073],{"class":102,"line":245},[218,1069,1070],{"class":227},"changed ",[218,1072,258],{"class":223},[218,1074,1075],{"class":227}," []\n",[218,1077,1078,1080,1082,1084],{"class":102,"line":252},[218,1079,813],{"class":223},[218,1081,816],{"class":227},[218,1083,819],{"class":223},[218,1085,1086],{"class":227}," parcels.getFeatures(QgsFeatureRequest().setFilterFids(\n",[218,1088,1089,1092,1095,1098,1100,1102,1104],{"class":102,"line":277},[218,1090,1091],{"class":227},"        [f[",[218,1093,1094],{"class":264},"\"fid\"",[218,1096,1097],{"class":227},"] ",[218,1099,813],{"class":223},[218,1101,816],{"class":227},[218,1103,819],{"class":223},[218,1105,1106],{"class":227}," invalid.getFeatures()])):\n",[218,1108,1109,1112,1114],{"class":102,"line":282},[218,1110,1111],{"class":227},"    before ",[218,1113,258],{"class":223},[218,1115,1116],{"class":227}," f.geometry()\n",[218,1118,1119,1122,1124],{"class":102,"line":299},[218,1120,1121],{"class":227},"    after ",[218,1123,258],{"class":223},[218,1125,1126],{"class":227}," before.makeValid()\n",[218,1128,1129,1132,1135,1138,1141],{"class":102,"line":308},[218,1130,1131],{"class":223},"    if",[218,1133,1134],{"class":227}," before.area() ",[218,1136,1137],{"class":223},">",[218,1139,1140],{"class":271}," 0",[218,1142,1143],{"class":227},":\n",[218,1145,1146,1149,1151,1154,1157,1159,1162,1165],{"class":102,"line":327},[218,1147,1148],{"class":227},"        delta ",[218,1150,258],{"class":223},[218,1152,1153],{"class":271}," abs",[218,1155,1156],{"class":227},"(after.area() ",[218,1158,696],{"class":223},[218,1160,1161],{"class":227}," before.area()) ",[218,1163,1164],{"class":223},"\u002F",[218,1166,1167],{"class":227}," before.area()\n",[218,1169,1170,1173,1176,1178,1181],{"class":102,"line":341},[218,1171,1172],{"class":223},"        if",[218,1174,1175],{"class":227}," delta ",[218,1177,1137],{"class":223},[218,1179,1180],{"class":271}," 0.01",[218,1182,1143],{"class":227},[218,1184,1185,1188,1190,1192,1195,1198,1201,1204,1206,1208],{"class":102,"line":354},[218,1186,1187],{"class":227},"            changed.append((f[",[218,1189,946],{"class":264},[218,1191,949],{"class":227},[218,1193,1194],{"class":271},"round",[218,1196,1197],{"class":227},"(delta ",[218,1199,1200],{"class":223},"*",[218,1202,1203],{"class":271}," 100",[218,1205,753],{"class":227},[218,1207,317],{"class":271},[218,1209,1210],{"class":227},")))\n",[218,1212,1213,1215,1217,1219,1222,1225],{"class":102,"line":367},[218,1214,429],{"class":271},[218,1216,432],{"class":227},[218,1218,897],{"class":271},[218,1220,1221],{"class":227},"(changed), ",[218,1223,1224],{"class":264},"\"features would change area by more than 1 %\"",[218,1226,495],{"class":227},[14,1228,1229,1231,1232,1235,1236,1239,1240,30],{},[505,1230,507],{}," The invalid output keeps the source ",[215,1233,1234],{},"fid"," field, which maps each invalid feature back to the original layer. Comparing areas before and after ",[215,1237,1238],{},"makeValid"," is a quick, quantitative test of whether a repair is cosmetic or substantive: a bow-tie polygon repaired into two triangles can lose or gain a large share of its area depending on how it was drawn. Features whose area changes by more than a chosen tolerance go on a list for manual review; the rest can be repaired in bulk with ",[215,1241,1242],{},"native:fixgeometries",[179,1244,1246],{"id":1245},"save-the-report","Save the report",[14,1248,1249],{},"A validity check is worth keeping. Writing the outputs and a summary table to a single GeoPackage gives anyone — a colleague, the data supplier, yourself in six months — the evidence in one file.",[208,1251,1253],{"className":210,"code":1252,"language":212,"meta":213,"style":213},"from datetime import date\nfrom qgis.core import QgsVectorLayer, QgsFeature\n\nreport = f\"\u002Fdata\u002Fqa\u002Fparcels_validity_{date.today():%Y%m%d}.gpkg\"\nfor layer, name in [(invalid, \"invalid_features\"), (errors, \"error_points\")]:\n    processing.run(\"native:savefeatures\", {\n        \"INPUT\": layer, \"OUTPUT\": report, \"LAYER_NAME\": name,\n        \"ACTION_ON_EXISTING_FILE\": 1})          # 1 = add new layer to the file\n\nsummary = QgsVectorLayer(\"None?field=reason:string&field=count:integer\", \"summary\", \"memory\")\nrows = []\nfor why, n in counts.most_common():\n    f = QgsFeature(summary.fields())\n    f.setAttributes([why, n])\n    rows.append(f)\nsummary.dataProvider().addFeatures(rows)\nprocessing.run(\"native:savefeatures\", {\"INPUT\": summary, \"OUTPUT\": report,\n                                       \"LAYER_NAME\": \"summary\",\n                                       \"ACTION_ON_EXISTING_FILE\": 1})\nprint(\"report written to\", report)\n",[215,1254,1255,1267,1278,1282,1321,1345,1355,1375,1391,1395,1420,1429,1439,1449,1454,1459,1464,1485,1496,1507],{"__ignoreMap":213},[218,1256,1257,1259,1262,1264],{"class":102,"line":220},[218,1258,234],{"class":223},[218,1260,1261],{"class":227}," datetime ",[218,1263,224],{"class":223},[218,1265,1266],{"class":227}," date\n",[218,1268,1269,1271,1273,1275],{"class":102,"line":231},[218,1270,234],{"class":223},[218,1272,237],{"class":227},[218,1274,224],{"class":223},[218,1276,1277],{"class":227}," QgsVectorLayer, QgsFeature\n",[218,1279,1280],{"class":102,"line":245},[218,1281,249],{"emptyLinePlaceholder":248},[218,1283,1284,1287,1289,1292,1295,1297,1300,1303,1306,1308,1311,1313,1316,1318],{"class":102,"line":252},[218,1285,1286],{"class":227},"report ",[218,1288,258],{"class":223},[218,1290,1291],{"class":223}," f",[218,1293,1294],{"class":264},"\"\u002Fdata\u002Fqa\u002Fparcels_validity_",[218,1296,441],{"class":271},[218,1298,1299],{"class":227},"date.today():",[218,1301,1302],{"class":223},"%",[218,1304,1305],{"class":227},"Y",[218,1307,1302],{"class":223},[218,1309,1310],{"class":227},"m",[218,1312,1302],{"class":223},[218,1314,1315],{"class":227},"d",[218,1317,453],{"class":271},[218,1319,1320],{"class":264},".gpkg\"\n",[218,1322,1323,1325,1328,1330,1333,1336,1339,1342],{"class":102,"line":277},[218,1324,813],{"class":223},[218,1326,1327],{"class":227}," layer, name ",[218,1329,819],{"class":223},[218,1331,1332],{"class":227}," [(invalid, ",[218,1334,1335],{"class":264},"\"invalid_features\"",[218,1337,1338],{"class":227},"), (errors, ",[218,1340,1341],{"class":264},"\"error_points\"",[218,1343,1344],{"class":227},")]:\n",[218,1346,1347,1350,1353],{"class":102,"line":282},[218,1348,1349],{"class":227},"    processing.run(",[218,1351,1352],{"class":264},"\"native:savefeatures\"",[218,1354,296],{"class":227},[218,1356,1357,1360,1363,1366,1369,1372],{"class":102,"line":299},[218,1358,1359],{"class":264},"        \"INPUT\"",[218,1361,1362],{"class":227},": layer, ",[218,1364,1365],{"class":264},"\"OUTPUT\"",[218,1367,1368],{"class":227},": report, ",[218,1370,1371],{"class":264},"\"LAYER_NAME\"",[218,1373,1374],{"class":227},": name,\n",[218,1376,1377,1380,1382,1385,1388],{"class":102,"line":308},[218,1378,1379],{"class":264},"        \"ACTION_ON_EXISTING_FILE\"",[218,1381,314],{"class":227},[218,1383,1384],{"class":271},"1",[218,1386,1387],{"class":227},"})          ",[218,1389,1390],{"class":323},"# 1 = add new layer to the file\n",[218,1392,1393],{"class":102,"line":327},[218,1394,249],{"emptyLinePlaceholder":248},[218,1396,1397,1400,1402,1405,1408,1410,1413,1415,1418],{"class":102,"line":341},[218,1398,1399],{"class":227},"summary ",[218,1401,258],{"class":223},[218,1403,1404],{"class":227}," QgsVectorLayer(",[218,1406,1407],{"class":264},"\"None?field=reason:string&field=count:integer\"",[218,1409,753],{"class":227},[218,1411,1412],{"class":264},"\"summary\"",[218,1414,753],{"class":227},[218,1416,1417],{"class":264},"\"memory\"",[218,1419,495],{"class":227},[218,1421,1422,1425,1427],{"class":102,"line":354},[218,1423,1424],{"class":227},"rows ",[218,1426,258],{"class":223},[218,1428,1075],{"class":227},[218,1430,1431,1433,1435,1437],{"class":102,"line":367},[218,1432,813],{"class":223},[218,1434,829],{"class":227},[218,1436,819],{"class":223},[218,1438,834],{"class":227},[218,1440,1441,1444,1446],{"class":102,"line":380},[218,1442,1443],{"class":227},"    f ",[218,1445,258],{"class":223},[218,1447,1448],{"class":227}," QgsFeature(summary.fields())\n",[218,1450,1451],{"class":102,"line":386},[218,1452,1453],{"class":227},"    f.setAttributes([why, n])\n",[218,1455,1456],{"class":102,"line":391},[218,1457,1458],{"class":227},"    rows.append(f)\n",[218,1460,1461],{"class":102,"line":414},[218,1462,1463],{"class":227},"summary.dataProvider().addFeatures(rows)\n",[218,1465,1466,1469,1471,1474,1477,1480,1482],{"class":102,"line":426},[218,1467,1468],{"class":227},"processing.run(",[218,1470,1352],{"class":264},[218,1472,1473],{"class":227},", {",[218,1475,1476],{"class":264},"\"INPUT\"",[218,1478,1479],{"class":227},": summary, ",[218,1481,1365],{"class":264},[218,1483,1484],{"class":227},": report,\n",[218,1486,1487,1490,1492,1494],{"class":102,"line":473},[218,1488,1489],{"class":264},"                                       \"LAYER_NAME\"",[218,1491,314],{"class":227},[218,1493,1412],{"class":264},[218,1495,338],{"class":227},[218,1497,1498,1501,1503,1505],{"class":102,"line":498},[218,1499,1500],{"class":264},"                                       \"ACTION_ON_EXISTING_FILE\"",[218,1502,314],{"class":227},[218,1504,1384],{"class":271},[218,1506,383],{"class":227},[218,1508,1510,1512,1514,1517],{"class":102,"line":1509},20,[218,1511,429],{"class":271},[218,1513,432],{"class":227},[218,1515,1516],{"class":264},"\"report written to\"",[218,1518,1519],{"class":227},", report)\n",[14,1521,1522,1524,1525,1528,1529,1533],{},[505,1523,507],{}," ",[215,1526,1527],{},"ACTION_ON_EXISTING_FILE: 1"," adds each output as a new table to the same GeoPackage instead of overwriting the file; the first call creates it. The summary is an attribute-only table, which GeoPackage stores happily and QGIS opens as a plain table. Dating the file name keeps successive checks side by side, so a later run can show whether a supplier's data is getting better. The ",[21,1530,1532],{"href":1531},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fbuild-data-quality-report-pyqgis\u002F","data quality report recipe"," extends this pattern to topology, duplicates and attribute rules in one file.",[179,1535,1537],{"id":1536},"check-many-layers-at-once","Check many layers at once",[14,1539,1540],{},"The same check applied to every polygon and line layer in a folder or project is the quickest health check of an unfamiliar dataset.",[208,1542,1544],{"className":210,"code":1543,"language":212,"meta":213,"style":213},"from pathlib import Path\nfrom qgis.core import QgsVectorLayer, QgsWkbTypes\n\nfor path in sorted(Path(\"\u002Fdata\u002Fdelivery\").glob(\"*.gpkg\")):\n    layer = QgsVectorLayer(str(path), path.stem, \"ogr\")\n    if not layer.isValid() or layer.geometryType() == QgsWkbTypes.PointGeometry:\n        continue\n    r = processing.run(\"qgis:checkvalidity\", {\n        \"INPUT_LAYER\": layer, \"METHOD\": 2,\n        \"VALID_OUTPUT\": \"memory:\", \"INVALID_OUTPUT\": \"memory:\", \"ERROR_OUTPUT\": \"memory:\"})\n    share = r[\"INVALID_COUNT\"] \u002F max(layer.featureCount(), 1)\n    print(f\"{path.name:\u003C32} {r['INVALID_COUNT']:>6} invalid ({share:.1%})\")\n",[215,1545,1546,1558,1569,1573,1599,1618,1640,1645,1658,1674,1702,1729],{"__ignoreMap":213},[218,1547,1548,1550,1553,1555],{"class":102,"line":220},[218,1549,234],{"class":223},[218,1551,1552],{"class":227}," pathlib ",[218,1554,224],{"class":223},[218,1556,1557],{"class":227}," Path\n",[218,1559,1560,1562,1564,1566],{"class":102,"line":231},[218,1561,234],{"class":223},[218,1563,237],{"class":227},[218,1565,224],{"class":223},[218,1567,1568],{"class":227}," QgsVectorLayer, QgsWkbTypes\n",[218,1570,1571],{"class":102,"line":245},[218,1572,249],{"emptyLinePlaceholder":248},[218,1574,1575,1577,1580,1582,1584,1587,1590,1593,1596],{"class":102,"line":252},[218,1576,813],{"class":223},[218,1578,1579],{"class":227}," path ",[218,1581,819],{"class":223},[218,1583,881],{"class":271},[218,1585,1586],{"class":227},"(Path(",[218,1588,1589],{"class":264},"\"\u002Fdata\u002Fdelivery\"",[218,1591,1592],{"class":227},").glob(",[218,1594,1595],{"class":264},"\"*.gpkg\"",[218,1597,1598],{"class":227},")):\n",[218,1600,1601,1604,1606,1608,1610,1613,1616],{"class":102,"line":277},[218,1602,1603],{"class":227},"    layer ",[218,1605,258],{"class":223},[218,1607,1404],{"class":227},[218,1609,902],{"class":271},[218,1611,1612],{"class":227},"(path), path.stem, ",[218,1614,1615],{"class":264},"\"ogr\"",[218,1617,495],{"class":227},[218,1619,1620,1622,1625,1628,1631,1634,1637],{"class":102,"line":282},[218,1621,1131],{"class":223},[218,1623,1624],{"class":223}," not",[218,1626,1627],{"class":227}," layer.isValid() ",[218,1629,1630],{"class":223},"or",[218,1632,1633],{"class":227}," layer.geometryType() ",[218,1635,1636],{"class":223},"==",[218,1638,1639],{"class":227}," QgsWkbTypes.PointGeometry:\n",[218,1641,1642],{"class":102,"line":299},[218,1643,1644],{"class":223},"        continue\n",[218,1646,1647,1650,1652,1654,1656],{"class":102,"line":308},[218,1648,1649],{"class":227},"    r ",[218,1651,258],{"class":223},[218,1653,290],{"class":227},[218,1655,293],{"class":264},[218,1657,296],{"class":227},[218,1659,1660,1663,1665,1668,1670,1672],{"class":102,"line":327},[218,1661,1662],{"class":264},"        \"INPUT_LAYER\"",[218,1664,1362],{"class":227},[218,1666,1667],{"class":264},"\"METHOD\"",[218,1669,314],{"class":227},[218,1671,317],{"class":271},[218,1673,338],{"class":227},[218,1675,1676,1679,1681,1684,1686,1688,1690,1692,1694,1696,1698,1700],{"class":102,"line":341},[218,1677,1678],{"class":264},"        \"VALID_OUTPUT\"",[218,1680,314],{"class":227},[218,1682,1683],{"class":264},"\"memory:\"",[218,1685,753],{"class":227},[218,1687,408],{"class":264},[218,1689,314],{"class":227},[218,1691,1683],{"class":264},[218,1693,753],{"class":227},[218,1695,420],{"class":264},[218,1697,314],{"class":227},[218,1699,1683],{"class":264},[218,1701,383],{"class":227},[218,1703,1704,1707,1709,1712,1715,1717,1719,1722,1725,1727],{"class":102,"line":354},[218,1705,1706],{"class":227},"    share ",[218,1708,258],{"class":223},[218,1710,1711],{"class":227}," r[",[218,1713,1714],{"class":264},"\"INVALID_COUNT\"",[218,1716,1097],{"class":227},[218,1718,1164],{"class":223},[218,1720,1721],{"class":271}," max",[218,1723,1724],{"class":227},"(layer.featureCount(), ",[218,1726,1384],{"class":271},[218,1728,495],{"class":227},[218,1730,1731,1733,1735,1737,1739,1741,1744,1747,1749,1752,1755,1757,1759,1761,1763,1766,1768,1771,1774,1776,1779],{"class":102,"line":367},[218,1732,839],{"class":271},[218,1734,432],{"class":227},[218,1736,435],{"class":223},[218,1738,438],{"class":264},[218,1740,441],{"class":271},[218,1742,1743],{"class":227},"path.name",[218,1745,1746],{"class":223},":\u003C32",[218,1748,453],{"class":271},[218,1750,1751],{"class":271}," {",[218,1753,1754],{"class":227},"r[",[218,1756,463],{"class":264},[218,1758,450],{"class":227},[218,1760,853],{"class":223},[218,1762,453],{"class":271},[218,1764,1765],{"class":264}," invalid (",[218,1767,441],{"class":271},[218,1769,1770],{"class":227},"share",[218,1772,1773],{"class":223},":.1%",[218,1775,453],{"class":271},[218,1777,1778],{"class":264},")\"",[218,1780,495],{"class":227},[14,1782,1783,1785,1786,30],{},[505,1784,507],{}," Skipping point layers saves time without missing anything significant. The share of invalid features is a better headline than the raw count: twelve invalid features in two hundred thousand is a repair job, twelve in forty is a conversation with the supplier. For hundreds of files, the same loop fits the patterns in ",[21,1787,1789],{"href":1788},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis\u002F","running an algorithm over a folder of files",[179,1791,1793],{"id":1792},"qgis-version-compatibility","QGIS version compatibility",[14,1795,1796,1798,1799,753,1802,1804,1805,1807,1808,1811],{},[215,1797,118],{}," has the same parameters throughout QGIS 3.x and QGIS 4. The ",[215,1800,1801],{},"VALID_COUNT",[215,1803,511],{}," and ",[215,1806,515],{}," outputs are available on all current releases. From QGIS 3.36 the toolbox also has a group of geometry checker algorithms for specific problems — small angles, duplicate vertices, segment length and others; list them with ",[215,1809,1810],{},"[a.id() for a in QgsApplication.processingRegistry().algorithms() if \"checkgeometry\" in a.id()]"," on your version before relying on a particular one.",[179,1813,1815],{"id":1814},"troubleshooting","Troubleshooting",[184,1817,1818,1827,1835,1843],{},[187,1819,1820,1823,1824,30],{},[505,1821,1822],{},"The algorithm finds nothing, but GEOS operations still fail."," You used the QGIS method with ring self-intersections ignored; rerun with ",[215,1825,1826],{},"METHOD: 2",[187,1828,1829,1832,1833,30],{},[505,1830,1831],{},"Error points are missing for some invalid features."," Some problems, such as too few points in a ring, have no single location and are reported only in ",[215,1834,519],{},[187,1836,1837,1524,1840,1842],{},[505,1838,1839],{},"Counts differ between two machines.",[215,1841,617],{}," was used, so each machine applied its own Digitizing setting.",[187,1844,1845,1524,1848,1851],{},[505,1846,1847],{},"The report GeoPackage has only one table.",[215,1849,1850],{},"ACTION_ON_EXISTING_FILE"," was left at its default, which overwrites the file.",[179,1853,1855],{"id":1854},"conclusion","Conclusion",[14,1857,1858,1859,1861],{},"Run ",[215,1860,118],{}," with an explicit method — GEOS rules before GEOS-based operations, QGIS rules to list every problem — summarise error points by reason, compare areas before trusting an automatic repair, and save the invalid features, error points and summary together in a dated GeoPackage.",[179,1863,1865],{"id":1864},"frequently-asked-questions","Frequently Asked Questions",[14,1867,1868,1871],{},[505,1869,1870],{},"Does Processing skip invalid features automatically?","\nIt depends on the \"Invalid features filtering\" setting, which can stop, skip or ignore. Running an explicit check first means you know what that setting will do.",[14,1873,1874,1877],{},[505,1875,1876],{},"Are invalid geometries a problem for display only?","\nNo. Display usually works; analysis is where invalid geometries produce wrong areas, missing intersections and failed dissolves.",[14,1879,1880,1883,1884,30],{},[505,1881,1882],{},"Can a valid geometry still be wrong?","\nYes — a valid polygon can overlap its neighbour or leave a gap. That is a topology question, covered in ",[21,1885,1887],{"href":1886},"\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Ffind-gaps-and-overlaps-between-polygons-pyqgis\u002F","finding gaps and overlaps",[14,1889,1890,1893,1894,1897],{},[505,1891,1892],{},"Should I check validity in PostGIS instead?","\nFor data already in PostGIS, ",[215,1895,1896],{},"ST_IsValidReason"," gives the same GEOS answer without moving data. The PyQGIS route suits files and mixed sources.",[179,1899,1901],{"id":1900},"related","Related",[184,1903,1904,1909,1914,1919,1924],{},[187,1905,1906,1908],{},[21,1907,24],{"href":23}," — the guide this recipe belongs to",[187,1910,1911],{},[21,1912,1913],{"href":28},"Fix Invalid Geometries in PyQGIS",[187,1915,1916],{},[21,1917,1918],{"href":1886},"Find Gaps and Overlaps Between Polygons in PyQGIS",[187,1920,1921],{},[21,1922,1923],{"href":1531},"Build a Data Quality Report in PyQGIS",[187,1925,1926],{},[21,1927,1929],{"href":1928},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fhandle-processing-feedback-and-errors-pyqgis\u002F","Handle Processing Feedback and Errors in PyQGIS",[1931,1932,1933],"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 pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sRjNt, html code.shiki .sRjNt{--shiki-default:#85E89D;--shiki-default-font-weight:bold}html pre.shiki code .sns5M, html code.shiki .sns5M{--shiki-default:#DBEDFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":213,"searchDepth":231,"depth":231,"links":1935},[1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947],{"id":181,"depth":231,"text":182},{"id":202,"depth":231,"text":203},{"id":527,"depth":231,"text":528},{"id":621,"depth":231,"text":622},{"id":970,"depth":231,"text":971},{"id":1245,"depth":231,"text":1246},{"id":1536,"depth":231,"text":1537},{"id":1792,"depth":231,"text":1793},{"id":1814,"depth":231,"text":1815},{"id":1854,"depth":231,"text":1855},{"id":1864,"depth":231,"text":1865},{"id":1900,"depth":231,"text":1901},"Find every invalid geometry in a layer with the Check Validity algorithm, choose between the QGIS and GEOS rule sets, turn the error points into a per-reason summary, and keep a record of what was wrong before anything is repaired.","md",{"slug":1951,"type":1952,"breadcrumb":1953,"datePublished":1954,"dateModified":1954},"check-geometry-validity-report-pyqgis","article","Geometry Validity Report","2026-10-02","\u002Fspatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-geometry-validity-report-pyqgis",{"title":5,"description":1948},"spatial-data-processing-automation\u002Fdata-quality-and-topology-validation\u002Fcheck-geometry-validity-report-pyqgis\u002Findex","yv3bQLlWID_0AJX-jfGfsr2biEtDqurRpcGdSV9feGs",1790966261370]