Find and Fix Invalid Geometries in PyQGIS

An invalid geometry is one that breaks the rules the simple-features model assumes: a polygon whose boundary crosses itself, a ring that touches itself at a point, a hole that falls outside its shell, or a shape with duplicate consecutive vertices. QGIS will happily draw all of them. GEOS will not process them, so the failure surfaces later — as an empty buffer, a clip that returns nothing, or a terse exception naming a coordinate pair with no indication of which feature it came from.

This page is a focused recipe within Geometry Operations and Spatial Predicates in PyQGIS. It covers detecting invalidity and reporting where it is, repairing individual geometries, repairing a whole layer, and dealing with the fact that a repair can change a geometry's type.

Four ways a polygon can be invalidFour panels each show one invalidity. A bowtie polygon whose boundary crosses itself, with the crossing point marked. A ring that pinches to touch itself at a single point. A polygon with a hole drawn entirely outside its outer shell. A boundary with two identical consecutive vertices marked at one corner.QGIS draws all four of these; GEOS refuses to process themself-intersectionthe classic bowtiering self-touchpinches to a pointhole outside shellinterior ring is not insideduplicate verticestwo nodes at one location

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A vector layer you suspect. CAD exports, hand-digitised data and anything that has round-tripped through a shapefile are the usual candidates.
  • Write access to an output location, since the repair produces a new layer rather than editing in place.

Detect and report where

isGeosValid() gives a boolean. validateGeometry() gives a list of errors, each with a message and usually a location — which is what you actually need to fix data rather than just discard it.

from qgis.core import QgsProject

layer = QgsProject.instance().mapLayersByName("parcels")[0]

problems = []
for feature in layer.getFeatures():
    geometry = feature.geometry()
    if geometry.isNull() or geometry.isEmpty():
        problems.append((feature.id(), "no geometry", None))
        continue
    for error in geometry.validateGeometry():
        location = error.where() if error.hasWhere() else None
        problems.append((feature.id(), error.what(), location))

for fid, message, where in problems[:20]:
    coords = f" at {where.x():.2f}, {where.y():.2f}" if where else ""
    print(f"feature {fid}: {message}{coords}")
print(f"{len(problems)} problem(s) across {layer.featureCount()} feature(s)")

Breakdown: validateGeometry() returns a list of QgsGeometry.Error objects and an empty list means valid — one geometry can have several distinct errors. hasWhere() guards the where() call, because some errors are structural and have no single coordinate. Checking isNull()/isEmpty() separately matters: a feature with no geometry is a different problem from a malformed one, and validateGeometry() reports nothing for it. Capping the printed output at twenty keeps a console session usable on a badly broken dataset while the full count still tells you the scale.

Repair one geometry

makeValid() returns a repaired copy using the GEOS validity routines. It does not modify the original.

from qgis.core import QgsProject, QgsWkbTypes

feature = next(layer.getFeatures())
geometry = feature.geometry()

if not geometry.isGeosValid():
    repaired = geometry.makeValid()
    print("valid now:", repaired.isGeosValid())
    print("type before:", QgsWkbTypes.displayString(geometry.wkbType()))
    print("type after: ", QgsWkbTypes.displayString(repaired.wkbType()))
    print("area before:", round(geometry.area(), 2))
    print("area after: ", round(repaired.area(), 2))

Breakdown: Comparing the WKB type before and after is the important part. Repairing a bowtie splits it into two triangles, so a Polygon becomes a MultiPolygon — and writing that back into a layer declared as single-part silently drops all but one part. Comparing the areas catches the other surprise: a self-intersecting polygon has no well-defined area, and the repaired version's may differ substantially from what area() reported before.

Repair a whole layer

For a layer, native:fixgeometries does the iteration and writes a clean output in one call.

import processing

fixed = processing.run("native:fixgeometries", {
    "INPUT": "/data/parcels.gpkg|layername=parcels",
    "METHOD": 1,          # 0 = linework, 1 = structure
    "OUTPUT": "/data/output/parcels_fixed.gpkg",
})["OUTPUT"]

Breakdown: METHOD chooses the repair strategy. Linework (0) preserves every input vertex and rebuilds the topology around them, which keeps boundaries exactly where the surveyor put them. Structure (1) is the newer GEOS approach; it can move or drop vertices but produces a cleaner result on badly tangled input. Linework is the safer default for cadastral data where vertex positions are authoritative; structure is better for messy imports. The algorithm silently drops features whose geometry cannot be repaired at all, so compare feature counts afterwards.

Linework repair versus structure repairA self-intersecting bowtie polygon is shown on the left. The linework method produces two separate triangles sharing the original crossing point, keeping every input vertex, and the result is a multi-polygon. The structure method produces a single simplified polygon, which may have moved vertices but remains single-part.The same broken input, two repair strategiesinputPolygon, invalidMETHOD 0 · lineworkevery input vertex keptMETHOD 1 · structuremay move or drop verticesMultiPolygon2 parts — check youroutput schema allows itPolygonstill single-part

Handle the type change

Because a repair can turn a single-part geometry into a multi-part one, a pipeline that writes into a fixed schema needs an explicit decision: promote the output schema to multi-part, or split the parts into separate features.

import processing

fixed = processing.run("native:fixgeometries", {
    "INPUT": "/data/parcels.gpkg|layername=parcels",
    "METHOD": 0,
    "OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]

single = processing.run("native:multiparttosingleparts", {
    "INPUT": fixed,
    "OUTPUT": "/data/output/parcels_clean.gpkg",
})["OUTPUT"]

print(fixed.featureCount(), "->", single.featureCount(), "feature(s)")

Breakdown: native:multiparttosingleparts explodes every multi-part geometry into one feature per part, copying the attributes onto each. The feature-count comparison tells you how many were split — and that number is worth logging, because two parcels now share one parcel identifier, which downstream joins will not expect. The alternative is to keep them multi-part and make sure the output format allows it; GeoPackage does, and a shapefile declared as Polygon does not.

Validity repair belongs at the start of a chain, not the middle. Running it once on ingest and working from the clean copy avoids re-diagnosing the same failure in every algorithm downstream — the pattern used in Chaining Processing Algorithms in PyQGIS.

Prevent invalidity rather than repair it

Most invalidity is created, not inherited. Three habits stop it appearing in the first place, which is worth far more than any repair routine.

Where invalidity comes from, and what stops itThree paired rows. Digitising without snapping produces near-miss vertices and is prevented by enabling snapping. Excessive coordinate precision produces near-duplicate vertices and is prevented by applying a precision model. Writing unvalidated output propagates errors downstream and is prevented by validating before the write step.Each source has a cheap preventativedigitising without snappingnear-miss vertices, slivers between neighboursenable project snappingvertices land exactly on their neighboursunbounded coordinate precisionvertices a nanometre apart, ring self-touchessnap to a precision gridnative:snappointstogrid before writingwriting unvalidated outputthe failure surfaces three algorithms latervalidate at the write stepreject or repair before it propagates

The precision one is the least known and the most effective on computed geometry. Overlay operations produce coordinates at full double precision, and two vertices a fraction of a nanometre apart are enough to make a ring self-touch:

import processing

snapped = processing.run("native:snappointstogrid", {
    "INPUT": "/data/output/overlay_result.gpkg",
    "HSPACING": 0.001,          # 1 mm in a metric CRS
    "VSPACING": 0.001,
    "OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]

clean = processing.run("native:fixgeometries", {
    "INPUT": snapped, "METHOD": 0,
    "OUTPUT": "/data/output/overlay_clean.gpkg",
})["OUTPUT"]

Breakdown: A 1 mm grid is far finer than any real-world survey tolerance yet coarse enough to collapse the sub-micron differences that overlay arithmetic introduces. Snapping first and repairing second is the right order — snapping removes the near-duplicates that would otherwise make the repair harder, and often leaves nothing for the repair to do. Adding this pair to the end of an overlay chain is the single most effective way to stop invalidity accumulating through a pipeline.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.28 LTR3.9native:fixgeometries has no METHOD parameter — it always uses linework.
3.34 LTR3.12Baseline. METHOD added, defaulting to linework.
3.40 / 3.443.12Structure method improved; makeValid() gained an explicit method argument.

validateGeometry(), isGeosValid() and makeValid() are available throughout 3.x with unchanged signatures.

Troubleshooting

  • makeValid() returns an empty geometry. The input has no recoverable area — a degenerate polygon of zero width, for example. Drop the feature and log its ID.
  • Feature count drops after fixing. Some geometries could not be repaired at all and were discarded. Compare counts explicitly and re-run the detection pass on the input to identify them.
  • A shapefile output loses parts. The format's declared geometry type is single-part. Write to GeoPackage, or explode with native:multiparttosingleparts first.
  • Areas change after repair. Expected for self-intersecting input, whose area was never well defined. Compare before and after and decide whether the difference is acceptable for your use.
  • Validation reports nothing but GEOS still fails. The failure is in the other operand of the operation, not this one. Validate both inputs.
  • Repair is slow on a large layer. validateGeometry() is much more expensive than isGeosValid(). Filter with the cheap boolean first and only collect detailed errors for the failures.

Conclusion

Detect with isGeosValid() for speed and validateGeometry() for detail, repair with makeValid() on a single geometry or native:fixgeometries on a layer, and always check what the repair did to the geometry type and area. Doing this once on ingest is far cheaper than diagnosing the same corrupted feature from three different algorithm failures later.

Frequently Asked Questions

What is the difference between isGeosValid and validateGeometry?isGeosValid() returns a boolean and is fast. validateGeometry() returns a list of specific errors, most of which carry a coordinate, and is considerably slower. Use the boolean to find the bad features and the detailed call only on those.

Which fix method should I use, linework or structure? Linework preserves every input vertex and rebuilds topology around them, which suits cadastral or surveyed data where vertex positions are authoritative. Structure produces cleaner results on badly tangled input but may move or drop vertices.

Why did my polygon become a multi-polygon? Repairing a self-intersecting shape splits it into the separate valid pieces it implied. That is the correct result, but it changes the geometry type — make sure the output format and any downstream schema accept multi-part geometry.

Can I fix geometries in place instead of writing a new layer?native:fixgeometries always writes an output. To edit in place, loop the features yourself, call makeValid() on each, and write the results back through the edit buffer — checking the type change on every one.