Debugging Shapefile Geometry Errors in QGIS and Python

TL;DR: Run QGIS’s GEOS-based Check Validity tool to classify errors by type, then apply shapely.make_valid() as the primary repair and .buffer(0) as a fallback in Python. Finish with shapely.set_precision(grid_size=0.001) to eliminate floating-point micro-duplicates, strip empty geometries, and re-assert is_valid on the whole layer before any equipment export.

Why This Comes Up in Ag-GIS Workflows

Corrupted geometries — self-intersections, invalid rings, duplicate vertices, or null features — are a routine output of the tools agtech teams use every day. Drone orthomosaics traced by auto-segmentation algorithms, RTK boundary surveys logged at 10 Hz, yield monitor passes converted from CSV to polygon, and prescription zones exported from web-based farm management platforms all introduce floating-point artifacts or topology violations that downstream systems refuse to process.

The consequences are concrete: a single self-intersecting polygon in a variable-rate prescription shapefile can cause the equipment’s ISOBUS task controller to reject the entire file at field startup, leaving the operator with no prescription and defaulting to a flat application rate. A layer with mixed Polygon/MultiPolygon types or null geometries breaks shapefile validation for farm equipment checks silently, allowing the corrupted file to reach the export stage where diagnosis is harder.

Without a systematic debug workflow, engineers typically waste time re-digitising boundaries or wrestling with opaque terminal error codes. The method below eliminates both.

Shapefile geometry debug and repair pipeline A flowchart showing five stages. Stage 1: Raw Shapefile. Stage 2: QGIS Check Validity using GEOS method, with a branch for valid features going directly to stage 5 and invalid features going to stage 3. Stage 3: Python repair — make_valid primary, buffer(0) fallback. Stage 4: set_precision grid_size 0.001 plus strip empty geometries. Stage 5: Re-validate is_valid assert then export to farm equipment. Raw Shapefile field boundary / prescription zone QGIS Check Validity (GEOS) invalid_output layer classifies error type per feature valid features invalid Python Repair make_valid() primary buffer(0) fallback strip empty / null Precision Normalise set_precision grid_size=0.001 Re-validate assert is_valid assert no empty → equipment export

① source ② classify ③ repair ④ normalise ⑤ gate + export

Prerequisites

This page assumes the environment described in Shapefile Validation for Farm Equipment is already in place. The only additions needed here are:

  • shapely>=2.0 — for shapely.make_valid() and shapely.set_precision() (both moved to the top-level shapely namespace in 2.x; the old shapely.validation.make_valid import still works but is deprecated)
  • geopandas>=0.14 — for GeoDataFrame.is_valid, .is_empty, and .notna() on geometry columns
  • QGIS 3.28 LTS or later — for the GEOS validation method in the Check Validity processing tool

No additional pip installs are required if geopandas is already installed with a GEOS-linked shapely.

Step 1 — Classify errors visually in QGIS

Before writing a single line of repair code, classify the error types. QGIS’s Check Validity tool runs GEOS directly and produces human-readable error labels per feature. This step saves time by distinguishing trivial floating-point artifacts from structural topology failures that require manual inspection.

  1. Open the Processing Toolbox (Ctrl+Alt+T on Linux/Windows, Cmd+Option+T on macOS) and search for Check Validity.
  2. Set your input layer to the shapefile under investigation. Under Method, choose GEOS — not the default QGIS method. GEOS matches the shapely backend used by Python and catches micro-self-intersections that the QGIS legacy method silently ignores.
  3. Run the tool. It produces three output layers: valid_output, invalid_output, and error_output. The error_output layer contains one point per error, labelled with the GEOS error string (e.g., Self-intersection, Ring self-intersection, Duplicate point, Too few points in geometry component).
  4. Select the invalid_output layer and open the attribute table. Sort by the _errors column. Group identical error types together — this shows whether you have one pathological feature or a systematic problem affecting the entire dataset.
  5. Use Identify Features to click individual error points in error_output and visually inspect the vertex geometry. Self-intersections at field boundary corners are usually digitising artifacts; repeated duplicates across many features indicate a CRS precision issue that set_precision will fix in bulk.

The Fix Geometries processing algorithm provides a quick GUI-based repair for small datasets, but it does not give you control over repair strategy, does not report which features changed, and cannot be integrated into an automated pipeline. Use it for one-off manual fixes only.

Step 2 — Repair programmatically in Python

The following self-contained script handles the complete repair sequence: load with explicit CRS check, apply make_valid, fall back to buffer(0) for survivors, normalise precision, strip unrepairable empties, and write the clean output. It is directly usable without scaffolding.

PYTHON
import geopandas as gpd
import shapely
from shapely.validation import explain_validity

# ── 1. Load shapefile with explicit CRS guard ──────────────────────────────
gdf = gpd.read_file("field_boundary.shp")

if gdf.crs is None:
    raise ValueError(
        "CRS is missing. Assign the correct EPSG code before validation: "
        "gdf = gdf.set_crs('EPSG:32614')"
    )

print(f"Loaded {len(gdf)} features  |  CRS: {gdf.crs.to_epsg()}")

# ── 2. Identify and report invalid geometries ──────────────────────────────
invalid_mask = ~gdf.geometry.is_valid

if invalid_mask.any():
    print(f"\nInvalid features: {invalid_mask.sum()} / {len(gdf)}")
    # Log a human-readable reason for each invalid feature
    for idx in gdf.index[invalid_mask]:
        reason = explain_validity(gdf.at[idx, "geometry"])
        print(f"  Feature {idx}: {reason}")

# ── 3. Primary repair: make_valid ──────────────────────────────────────────
# shapely.make_valid resolves self-intersections and ring orientation issues
# by decomposing the polygon into valid sub-geometries (may produce
# GeometryCollection on severely broken inputs).
    gdf.loc[invalid_mask, "geometry"] = (
        gdf.loc[invalid_mask, "geometry"].apply(shapely.make_valid)
    )

    # ── 4. Fallback: buffer(0) for features still invalid after make_valid ──
    still_invalid = ~gdf.geometry.is_valid
    if still_invalid.any():
        print(f"  buffer(0) fallback on {still_invalid.sum()} remaining features")
        gdf.loc[still_invalid, "geometry"] = (
            gdf.loc[still_invalid, "geometry"].buffer(0)
        )

# ── 5. Normalise coordinate precision ─────────────────────────────────────
# grid_size=0.001 snaps coordinates to 1mm grid in projected CRS (metres).
# This eliminates floating-point micro-duplicates that pass is_valid but
# cause equipment terminals to reject the file.
gdf["geometry"] = gdf.geometry.apply(
    lambda geom: shapely.set_precision(geom, grid_size=0.001)
)

# ── 6. Strip empty and null geometries ────────────────────────────────────
# make_valid can return GEOMETRYCOLLECTION EMPTY for degenerate inputs.
# Equipment controllers have no concept of a null feature row.
n_before = len(gdf)
gdf = gdf[~gdf.geometry.is_empty & gdf.geometry.notna()].copy()
if len(gdf) < n_before:
    print(f"Dropped {n_before - len(gdf)} empty/null features after repair")

# ── 7. Final assertion gate ────────────────────────────────────────────────
remaining_invalid = (~gdf.geometry.is_valid).sum()
assert remaining_invalid == 0, (
    f"{remaining_invalid} features are still invalid after repair. "
    "Inspect these manually in QGIS before proceeding."
)

print(f"\nAll {len(gdf)} features are valid. Writing clean output.")
gdf.to_file("field_boundary_clean.shp", driver="ESRI Shapefile")

Inline verification — add this immediately after the assert to confirm the output file can be round-tripped:

PYTHON
verify = gpd.read_file("field_boundary_clean.shp")
assert verify.geometry.is_valid.all(), "Round-trip read found invalid geometries"
assert not verify.geometry.is_empty.any(), "Round-trip read found empty geometries"
print(f"Verified: {len(verify)} clean features, CRS {verify.crs.to_epsg()}")

Gotchas and Edge Cases

  • make_valid can return a GeometryCollection instead of a Polygon when the input is severely broken (for example, a figure-eight field boundary). GeometryCollection is not a valid prescription geometry type. After make_valid, check gdf.geom_type and filter out or flatten collections: gdf = gdf[gdf.geom_type.isin(["Polygon", "MultiPolygon"])]. A subsequent .explode() splits MultiPolygon into single parts if needed.

  • set_precision changes vertex coordinates, which means area calculations performed before this step will differ from those after. Always run set_precision before computing agronomic area metrics, not after. For management zone classification algorithms that rely on zone area for rate calculation, re-compute zone areas from the normalised geometry.

  • RTK surveys at 10 Hz generate true duplicate vertices, not floating-point artifacts. set_precision alone does not deduplicate them. Use shapely.simplify(geom, tolerance=0.01, preserve_topology=True) before set_precision on RTK-derived boundaries to remove duplicate coordinate pairs without altering the visible boundary shape.

  • buffer(0) silently changes ring winding order on some geometries. If your downstream workflow uses signed area or winding-order-dependent algorithms (for example, certain ISOXML task controller parsers), verify that the repaired polygon exterior ring is counter-clockwise using shapely.is_ccw(geom.exterior) and re-orient if necessary with shapely.geometry.polygon.orient(geom, sign=1.0).

  • CRS must be in a projected metre-based system before set_precision with grid_size=0.001. If your shapefile is in geographic WGS84 (EPSG:4326), a grid size of 0.001 degrees corresponds to roughly 111 metres — far too coarse and will snap vertices to a 111 m grid, destroying field boundary precision. Reproject to the appropriate UTM zone using the approach in understanding CRS in precision agriculture before normalising precision.

This guide is part of Shapefile Validation for Farm Equipment — see there for the full validation pipeline covering CRS harmonisation, attribute constraint enforcement, and controlled export to ISOBUS-compatible formats.