Fixing Invalid Geometries in PyQGIS

Detect and repair invalid geometries in PyQGIS: isGeosValid, QgsGeometry.makeValid, the native:fixgeometries algorithm, and validating a whole layer before…

TL;DR: Test each feature with geometry.isGeosValid(); repair a single geometry with geometry.makeValid(); repair an entire layer with processing.run("native:fixgeometries", {...}); and always validate before you run spatial predicates, overlays, or a join — invalid geometry is the single most common cause of silently wrong analysis results.

This page is part of the Geometry Operations and Validation in PyQGIS guide, which sits inside the broader PyQGIS Core Architecture & Data Handling reference. It focuses on one narrow but high-value task: turning a layer full of self-intersecting rings and nested holes into geometry that GEOS-backed operations will accept without throwing or returning garbage.

Why invalid geometry breaks everything downstream

Almost every meaningful spatial operation in QGIS — intersects(), within(), buffer(), dissolve, clip, union, spatial joins — is delegated to the GEOS library. GEOS assumes the OGC Simple Features validity rules: rings do not self-intersect, polygon interiors are connected, holes lie inside their shell and do not touch it in more than one point. Feed GEOS a geometry that violates those rules and you get one of three bad outcomes: an exception, a False predicate result that should have been True, or an empty/degenerate output geometry. None of them are announced loudly, which is why a batch job can appear to “succeed” while producing nonsense.

The defensive habit is simple: validate before you compute. The rest of this page shows how to detect invalidity per feature, how to interpret the error report, and how to repair either a single geometry or an entire layer.

Complete Runnable Repair Utility

Drop this into a headless script, the QGIS Python console, or a processing script. It iterates a layer, reports the specific validity error on each broken feature, repairs it with makeValid(), and writes a clean copy. The layer-scale native:fixgeometries call follows immediately after as an alternative one-liner.

python
"""
fix_invalid_geometries.py

Detect and repair invalid vector geometries in PyQGIS.
Compatible with QGIS 3.28+ / GEOS 3.10+.

Two strategies are shown:
  1. validate_and_repair_layer() - per-feature control with error reporting
  2. fix_layer_with_processing()  - one-call layer repair via native:fixgeometries
"""
from __future__ import annotations

from typing import Iterator

from qgis.core import (
    QgsFeature,
    QgsFeatureSink,
    QgsGeometry,
    QgsProject,
    QgsVectorFileWriter,
    QgsVectorLayer,
    QgsWkbTypes,
)
import processing


def iter_invalid_features(layer: QgsVectorLayer) -> Iterator[tuple[QgsFeature, str]]:
    """
    Yield (feature, error_message) for every feature whose geometry
    fails the GEOS validity test.

    Uses isGeosValid() as the fast gate, then validateGeometry() to
    obtain a human-readable description of the first error found.
    """
    for feature in layer.getFeatures():
        geom: QgsGeometry = feature.geometry()
        if geom.isNull() or geom.isEmpty():
            yield feature, "null or empty geometry"
            continue
        if not geom.isGeosValid():
            errors = geom.validateGeometry()
            message = errors[0].what() if errors else "invalid (unspecified)"
            yield feature, message


def validate_and_repair_layer(
    layer: QgsVectorLayer,
    out_path: str,
) -> tuple[int, str]:
    """
    Repair every invalid feature in `layer` with makeValid() and write
    the result to a GeoPackage at `out_path`.

    Returns:
        (repaired_count, out_path). repaired_count is how many features
        needed makeValid() applied.

    Raises:
        RuntimeError: if the output file cannot be written.
    """
    fields = layer.fields()
    # makeValid() may promote the type (a bowtie polygon can become a
    # multipolygon), so declare the output as the multi-variant to be safe.
    out_wkb = QgsWkbTypes.multiType(layer.wkbType())

    options = QgsVectorFileWriter.SaveVectorOptions()
    options.driverName = "GPKG"
    options.layerName = f"{layer.name()}_fixed"

    writer = QgsVectorFileWriter.create(
        out_path,
        fields,
        out_wkb,
        layer.sourceCrs(),
        QgsProject.instance().transformContext(),
        options,
    )
    if writer.hasError() != QgsVectorFileWriter.NoError:
        raise RuntimeError(f"Cannot create {out_path}: {writer.errorMessage()}")

    repaired = 0
    for feature in layer.getFeatures():
        geom: QgsGeometry = feature.geometry()
        if not geom.isNull() and not geom.isGeosValid():
            fixed = geom.makeValid()
            # Force to multi so it matches the declared output WKB type.
            fixed.convertToMultiType()
            feature.setGeometry(fixed)
            repaired += 1
        else:
            feature.setGeometry(geom)
        writer.addFeature(feature, QgsFeatureSink.FastInsert)

    del writer  # flush and close the datasource
    return repaired, out_path


def fix_layer_with_processing(
    layer: QgsVectorLayer,
    out_path: str,
) -> QgsVectorLayer:
    """
    Repair an entire layer in a single call using the native
    fixgeometries processing algorithm.

    METHOD 1 (structure) rebuilds geometries following the GEOS
    MakeValid 'structure' approach; the default keeps input parts.
    """
    result = processing.run(
        "native:fixgeometries",
        {
            "INPUT": layer,
            "METHOD": 1,          # 0 = Linework, 1 = Structure
            "OUTPUT": out_path,
        },
    )
    return QgsVectorLayer(result["OUTPUT"], f"{layer.name()}_fixed", "ogr")


if __name__ == "__console__":
    src = QgsVectorLayer("/data/parcels.gpkg|layername=parcels", "parcels", "ogr")
    assert src.isValid(), "source layer failed to load"

    for feat, why in iter_invalid_features(src):
        print(f"feature {feat.id()}: {why}")

    count, path = validate_and_repair_layer(src, "/data/parcels_fixed.gpkg")
    print(f"Repaired {count} features -> {path}")

Repair Decision Flow

The diagram below is the triage path for a single feature: test validity, classify the error if any, repair, and re-check. Re-checking after makeValid() is not optional — a repair can occasionally still leave a sliver that a strict consumer rejects, and you want that caught before analysis, not during it.

Invalid Geometry Repair Flow Flowchart: test isGeosValid on a feature. If valid, use it directly. If invalid, classify the error type, apply makeValid, re-check validity, and only then pass it to spatial analysis. feature geometry isGeosValid()? test the feature yes use directly no classify error via validateGeometry() self-intersection / nested hole geom.makeValid() valid now? no yes analyse

Architecture Breakdown

isGeosValid() and QgsGeometry.validateGeometry()

These are the two detection tools, and they answer different questions. QgsGeometry.isGeosValid() returns a single bool computed by the GEOS isValid predicate — it is fast, authoritative for anything you plan to feed to GEOS, and the correct gate to place in front of every spatial operation. It takes an optional flags argument; passing Qgis.GeometryValidityFlag.AllowSelfTouchingHoles relaxes one specific OGC rule if your workflow tolerates it.

QgsGeometry.validateGeometry() returns a list[QgsGeometry.Error] describing what is wrong and where. Each Error exposes what() (a message such as "Self-intersection", "Ring self-intersection", or "Nested holes") and, where applicable, where() (a QgsPointXY at the offending location). Use it for reporting and debugging, not as a hot-path gate — it is more expensive than isGeosValid(). It accepts a method argument: Qgis.GeometryValidationEngine.Geos uses GEOS (matches isGeosValid), while Qgis.GeometryValidationEngine.QgisInternal uses the native QGIS checker, which reports more error categories but at a different cost profile.

The important gotcha: the two engines do not always agree on edge cases. For pipelines that hand geometry to GEOS operations, trust GEOS — validate with the same engine that will consume the result.

The common error types you will encounter:

  • Self-intersection — a “bowtie” polygon where the exterior ring crosses itself. GEOS cannot compute a well-defined interior.
  • Ring self-intersection — a single ring touches itself, pinching the interior into disconnected pieces.
  • Nested holes / holes outside shell — an interior ring lies outside the exterior ring, or one hole sits inside another, which is undefined under Simple Features.

makeValid() behaviour

QgsGeometry.makeValid() returns a new valid QgsGeometry built from the input — it does not mutate in place, so you must capture the return value. Under the hood it delegates to the GEOS MakeValid routine. Two behaviours must be planned for:

  1. The geometry type may change. Repairing a self-intersecting polygon can split it into two disjoint parts, so a Polygon input can come back as a MultiPolygon. A degenerate polygon whose repair collapses to a line can even come back as a LineString. This is why the repair utility above declares its output as the multi-variant WKB type and calls convertToMultiType() — a writer configured for singlepart polygons will silently reject a multipart repair.
  2. It may return empty. If a geometry is so degenerate that no valid interior survives (for example a zero-area sliver), makeValid() can return a null or empty geometry. Always re-test with isGeosValid() and handle the empty case explicitly rather than writing an empty feature into your output.

Recent QGIS/GEOS versions expose a method and keepCollapsed parameter on makeValid() (structure vs linework). The structure method rebuilds the geometry from its boundary and generally yields cleaner polygons for GIS use; linework preserves all input line segments. When the API version supports it, prefer structure for polygon layers.

native:fixgeometries at layer scale

For repairing a whole layer in one shot, the processing algorithm native:fixgeometries is the idiomatic tool. Called through processing.run("native:fixgeometries", {...}), it streams every feature through the same repair logic as makeValid() and writes a repaired output layer, preserving all attributes. Its key parameters:

  • INPUT — the source layer or a layer path.
  • METHOD0 for Linework, 1 for Structure, mirroring the single-geometry methods above.
  • OUTPUT — a destination path (GeoPackage, shapefile) or "TEMPORARY_OUTPUT" for an in-memory result.

The algorithm returns a dict whose OUTPUT key holds the result layer or path. Because it always promotes to the multi-variant output type where needed, it sidesteps the type-mismatch trap you have to handle manually with the per-feature approach. Reach for native:fixgeometries when you want a clean layer with no bespoke logic; reach for the per-feature loop when you need to report which features were broken and why, or apply conditional handling.

Pre-flight Validation Before a Spatial Join

The highest-value place to run repair is immediately before an operation that trusts GEOS. A spatial join is the classic example: joining attributes by intersects against invalid polygons produces missing or duplicated matches with no error raised. Wire validation as a pre-flight stage, then hand the clean layer to the join. This pairs naturally with a spatial index — see Spatial Indexing and Query Optimization in PyQGIS for building the QgsSpatialIndex that accelerates the candidate lookup once the geometries are trustworthy.

python
"""
Pre-flight repair then spatial join, in one processing pipeline.
"""
from __future__ import annotations

from qgis.core import QgsVectorLayer
import processing


def joined_on_clean_geometry(
    left: QgsVectorLayer,
    right: QgsVectorLayer,
) -> QgsVectorLayer:
    """
    Repair both inputs, then run an attribute-preserving spatial join.

    Returns a memory layer of `left` features enriched with attributes
    from intersecting `right` features. Repairing first prevents the
    silent match losses that invalid polygons cause in GEOS predicates.
    """
    left_fixed = processing.run(
        "native:fixgeometries",
        {"INPUT": left, "METHOD": 1, "OUTPUT": "TEMPORARY_OUTPUT"},
    )["OUTPUT"]
    right_fixed = processing.run(
        "native:fixgeometries",
        {"INPUT": right, "METHOD": 1, "OUTPUT": "TEMPORARY_OUTPUT"},
    )["OUTPUT"]

    joined = processing.run(
        "native:joinattributesbylocation",
        {
            "INPUT": left_fixed,
            "JOIN": right_fixed,
            "PREDICATE": [0],          # 0 = intersects
            "METHOD": 0,               # one-to-many
            "DISCARD_NONMATCHING": False,
            "OUTPUT": "TEMPORARY_OUTPUT",
        },
    )["OUTPUT"]
    return joined

Production Best Practices

  • Validate before you compute, not after it fails. Put an isGeosValid() gate in front of every predicate, overlay, or join rather than debugging empty outputs later.
  • Use isGeosValid() as the gate, validateGeometry() for the report. The first is cheap and matches GEOS; reserve the second for logging which features broke and where.
  • Always capture the return of makeValid(). It builds a new geometry — the original is unchanged. Re-test the result with isGeosValid() and handle the empty/degenerate case.
  • Plan for a type change. Declare repaired outputs as the multi-variant WKB type and call convertToMultiType(), or a singlepart writer will drop your repaired features.
  • Prefer the structure method (METHOD=1) for polygon layers. It rebuilds cleaner interiors than linework for typical GIS data.
  • Reach for native:fixgeometries for whole layers. It preserves attributes and handles type promotion automatically; save the per-feature loop for when you need error reporting or conditional logic.
  • Log repairs with QgsMessageLog. A count of how many features needed fixing is a useful data-quality signal that belongs in your pipeline logs, not swallowed silently.