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.

Three outputs from one checkThe 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.One run, three layersinput layerparcels.gpkgqgis:checkvalidityMETHOD: QGIS or GEOSVALID_OUTPUTunchanged featuresINVALID_OUTPUT+ _errors fieldERROR_OUTPUTa point per problemthe report is built from the last two

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.

QGIS rules and GEOS rulesThe 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.Same polygon, two opinionsGEOS (METHOD 2)OGC simple features rulesmatches PostGIS ST_IsValidfirst problem per featureuse when data goes to GEOS toolsQGIS (METHOD 1)QGIS's own validatorevery problem it findscan ignore ring self-touchuse to list all issuesMETHOD 0 follows the Digitizing settings — avoid it in scripts

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 summary to actionIf 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.What the summary says to dosmall, localduplicate nodessingle-vertex touchesrepair automaticallyshape-changingbow-ties, nested shellsholes outside shellsrepair, compare areaswidespreada large share invalidsystematic patternsend back with report

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: 0 was used, so each machine applied its own Digitizing setting.
  • The report GeoPackage has only one table. ACTION_ON_EXISTING_FILE was 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.