Build a Geometry Validity Report in PyQGIS
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.
This recipe belongs to 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 fixing invalid geometries.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series, with the Processing plugin enabled (it is by default).
- 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.
- Standalone scripts need Processing initialised, as described in running Python scripts outside QGIS Desktop.
Run the check
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.
import processing
from qgis.core import QgsProject
parcels = QgsProject.instance().mapLayersByName("parcels")[0]
result = processing.run("qgis:checkvalidity", {
"INPUT_LAYER": parcels,
"METHOD": 2, # 1 = QGIS rules, 2 = GEOS rules
"IGNORE_RING_SELF_INTERSECTION": False,
"VALID_OUTPUT": "memory:valid",
"INVALID_OUTPUT": "memory:invalid",
"ERROR_OUTPUT": "memory:errors",
})
valid, invalid, errors = (result["VALID_OUTPUT"], result["INVALID_OUTPUT"],
result["ERROR_OUTPUT"])
print(f"{result['VALID_COUNT']} valid, {result['INVALID_COUNT']} invalid, "
f"{result['ERROR_COUNT']} problems")
QgsProject.instance().addMapLayers([invalid, errors])
Breakdown: The three count outputs give the headline numbers without iterating anything. INVALID_COUNT counts features; 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 _errors field added; the errors layer holds points with a message field, placed where each problem is. Adding both to the map makes inspection quick: style the error points boldly and zoom through them.
Choose the rule set deliberately
The 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.
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. 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.
Avoid METHOD: 0 in scripts. It means "whatever the user's Digitizing settings say", so the same script gives different results on different machines.
Summarise errors by reason
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.
import re
from collections import Counter
def reason(message):
"""Strip coordinates so equal problems group together."""
text = re.sub(r"\[?-?\d+(\.\d+)?[ ,]+-?\d+(\.\d+)?\]?", "", message)
return re.sub(r"\s+", " ", text).strip(" :.")
counts = Counter(reason(f["message"]) for f in errors.getFeatures())
for why, n in counts.most_common():
print(f"{n:>6} {why}")
worst = sorted(invalid.getFeatures(), key=lambda f: len(str(f["_errors"])), reverse=True)[:5]
for f in worst:
print(f["parcel_id"], str(f["_errors"])[:80])
Breakdown: 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.
Decide whether automatic repair is safe
Not every invalid geometry should be repaired automatically. The summary tells you which case you are in.
from qgis.core import QgsFeatureRequest
changed = []
for f in parcels.getFeatures(QgsFeatureRequest().setFilterFids(
[f["fid"] for f in invalid.getFeatures()])):
before = f.geometry()
after = before.makeValid()
if before.area() > 0:
delta = abs(after.area() - before.area()) / before.area()
if delta > 0.01:
changed.append((f["parcel_id"], round(delta * 100, 2)))
print(len(changed), "features would change area by more than 1 %")
Breakdown: The invalid output keeps the source fid field, which maps each invalid feature back to the original layer. Comparing areas before and after 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 native:fixgeometries.
Save the report
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.
from datetime import date
from qgis.core import QgsVectorLayer, QgsFeature
report = f"/data/qa/parcels_validity_{date.today():%Y%m%d}.gpkg"
for layer, name in [(invalid, "invalid_features"), (errors, "error_points")]:
processing.run("native:savefeatures", {
"INPUT": layer, "OUTPUT": report, "LAYER_NAME": name,
"ACTION_ON_EXISTING_FILE": 1}) # 1 = add new layer to the file
summary = QgsVectorLayer("None?field=reason:string&field=count:integer", "summary", "memory")
rows = []
for why, n in counts.most_common():
f = QgsFeature(summary.fields())
f.setAttributes([why, n])
rows.append(f)
summary.dataProvider().addFeatures(rows)
processing.run("native:savefeatures", {"INPUT": summary, "OUTPUT": report,
"LAYER_NAME": "summary",
"ACTION_ON_EXISTING_FILE": 1})
print("report written to", report)
Breakdown: 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 data quality report recipe extends this pattern to topology, duplicates and attribute rules in one file.
Check many layers at once
The same check applied to every polygon and line layer in a folder or project is the quickest health check of an unfamiliar dataset.
from pathlib import Path
from qgis.core import QgsVectorLayer, QgsWkbTypes
for path in sorted(Path("/data/delivery").glob("*.gpkg")):
layer = QgsVectorLayer(str(path), path.stem, "ogr")
if not layer.isValid() or layer.geometryType() == QgsWkbTypes.PointGeometry:
continue
r = processing.run("qgis:checkvalidity", {
"INPUT_LAYER": layer, "METHOD": 2,
"VALID_OUTPUT": "memory:", "INVALID_OUTPUT": "memory:", "ERROR_OUTPUT": "memory:"})
share = r["INVALID_COUNT"] / max(layer.featureCount(), 1)
print(f"{path.name:<32} {r['INVALID_COUNT']:>6} invalid ({share:.1%})")
Breakdown: 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 running an algorithm over a folder of files.
QGIS version compatibility
qgis:checkvalidity has the same parameters throughout QGIS 3.x and QGIS 4. The VALID_COUNT, INVALID_COUNT and ERROR_COUNT 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 [a.id() for a in QgsApplication.processingRegistry().algorithms() if "checkgeometry" in a.id()] on your version before relying on a particular one.
Troubleshooting
- The algorithm finds nothing, but GEOS operations still fail. You used the QGIS method with ring self-intersections ignored; rerun with
METHOD: 2. - 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
_errors. - Counts differ between two machines.
METHOD: 0was used, so each machine applied its own Digitizing setting. - The report GeoPackage has only one table.
ACTION_ON_EXISTING_FILEwas left at its default, which overwrites the file.
Conclusion
Run qgis:checkvalidity 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.
Frequently Asked Questions
Does Processing skip invalid features automatically? It 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.
Are invalid geometries a problem for display only? No. Display usually works; analysis is where invalid geometries produce wrong areas, missing intersections and failed dissolves.
Can a valid geometry still be wrong? Yes — a valid polygon can overlap its neighbour or leave a gap. That is a topology question, covered in finding gaps and overlaps.
Should I check validity in PostGIS instead?
For data already in PostGIS, ST_IsValidReason gives the same GEOS answer without moving data. The PyQGIS route suits files and mixed sources.