Geometry validity: find and understand invalid geometries

tool
geometry
bilingual
An interactive lab with Python, R and MapLibre: upload a GeoJSON, find out which geometries are invalid and why, compare how the two languages diagnose the same problem. Repair is a separate, optional step.
Published

August 29, 2026

Upload a GeoJSON, find out which geometries are invalid and why, and run the same diagnosis in Python and in R on the same map. Repair (make_valid()/st_make_valid()) is a separate, optional step, at the bottom of the page.

A .geojson, or a shapefile (either as a single .zip, or as .shp/.dbf/.shx selected together): see the section below.

Just want to quickly validate or fix a file, without the rest of the reading? Use the standalone tool — same engine, less prose.

Can a polygon look normal and still be invalid?

flowchart LR
    A["🗺️ Looks fine on the map"] --> B{"Validity check"}
    B --> C["❌ INVALID: why?"]

Yes: a polygon can have a boundary that crosses itself, a hole that extends outside the outer boundary, or two holes that overlap, and it’s not uncommon for the shape on the map to look perfectly fine. Below you can find out exactly what to look for, in your own file or in one of the six ready-made examples further down.

Shared map and table

A single map and table, shared by every example on the page: run the same diagnosis in Python or R and compare the result here. The two languages draw to two separate layers: running one doesn’t clear or change what the other already drew, so both stay visible together (and in their own tab in the table below) for a direct comparison. Running R doesn’t “correct” or replace Python’s result, and vice versa. If you want to see a language’s result change, re-run that language’s own cell.

Diagnose: validate your geometry

Run the same check in Python or R on the current data: your file uploaded above, or, if you haven’t uploaded one, the bowtie example below (the diagnosis cells load it automatically when there’s no upload). No repair yet, just diagnosis: is the geometry valid? If not, why?

Same spatial question, two languages

Python and R answer the same question with one line each:

Python

gdf.geometry.is_valid

R

sf::st_is_valid(data)

Both go through the same geometry engine, GEOS, for this calculation.1

1 Don’t expect the exact same text when you ask why a geometry is invalid, though: explain_validity() in Python includes the coordinates of the problem (e.g. "Self-intersection[12.5 41.9]"), while st_is_valid(reason = TRUE) in R describes the edges involved without coordinates (e.g. "Edge 0 crosses edge 2"): same calculation, different vocabulary. Not a bug in either one, just a detail that distinguishes one spatial ecosystem from the other.

Valid according to whom?

“Valid” isn’t absolute: it depends on the geometric model and the engine checking it.

  • Python → shapely → always GEOS.
  • R → sf → GEOS or S2, depending on sf::sf_use_s2().

For the diagnosis above the difference doesn’t show. It becomes concrete in the repair, below:

Important

Why sf_use_s2(FALSE) before st_make_valid()? With geographic coordinates (lon/lat) and sf_use_s2(TRUE) (the default), st_make_valid() in R dispatches to s2::s2_rebuild() instead of GEOS: a different algorithm that, on these cases, fixes nothing, returning the geometry unchanged without an error. Verified not just on webR but also on real R 4.3.3 with sf 1.0.15: this isn’t a bug in this project, it’s documented sf behavior (see issues #1985 and #1771 on r-spatial/sf). Forcing sf_use_s2(FALSE) routes the call through GEOS, which works correctly here; it’s turned back on right after, because other operations (distances, areas) on geographic coordinates benefit from S2. shapely in Python doesn’t have this problem because it has no alternative S2 engine: it always goes through GEOS.

Repair (optional)

The diagnosis tells you what’s wrong. Repair changes the data: make_valid()/st_make_valid() rebuild the geometry into an equivalent, valid form. Requires having run the diagnosis above at least once: it reuses exactly the same data just diagnosed (gdf in Python, data in R stay in the session between one cell and the next, like in a notebook), with no new reading of the file.

The table and the report above update themselves: the valid_before/valid_after columns automatically appear next to the diagnosis ones.

Where R and Python actually disagree

Two examples in the gallery above (“A hole with no crossing at all” and “Holes are nested”) show R’s default marking valid (✓) a geometry Python marks invalid (✗). Not a wording difference like the rest of the gallery: a genuine disagreement on whether the geometry is even invalid, worth its own explanation instead of a quick aside inside those two sections.

Neither engine is wrong. S2Polygon’s own validity rule for a set of loops is just “no pair of loops that cross”; a hole that doesn’t touch the shell trivially satisfies that, whatever else it is. GEOS enforces the stricter OGC Simple Features rule instead: a Polygon’s hole is supposed to describe a piece cut out of its shell, so one that doesn’t touch the shell at all can’t be describing that, crossing or not. Same story with nested holes: S2Polygon’s own model builds a polygon’s interior from an odd/even rule over nested loop depth: shells at depth 0, holes at depth 1, and a shell at depth 2 is just as legitimate a structure as depth 0, not a violation.2 GEOS’s OGC rule says a single Polygon may have a shell and holes, full stop; a shell-within-a-hole has to be a separate Polygon, combined into a MultiPolygon instead. Two different definitions of “valid polygon,” not one calculation read two ways.

2 From Google’s own S2 documentation: “the given loops are automatically converted into a canonical form consisting of ‘shells’ and ‘holes’ … nested hierarchically. ‘Outer shell’ loops have depth 0, holes within those loops have depth 1, shells within those holes have depth 2, etc.” (see s2geometry.io/devguide/basic_types). Verified live against the nested-holes example, not just read off the docs: reversing every ring’s winding order in all four combinations still leaves R’s default S2 check reporting “Valid Geometry”, so it isn’t a ring-orientation artifact either; it’s the nesting itself that S2 accepts and GEOS doesn’t.

sf_use_s2(FALSE) (see “Valid according to whom?” above) makes R’s answer match Python’s in both cases.

Neither case is shown in the Repair section above: there’s nothing to repair under R’s default (neither is flagged invalid there to begin with), and Python’s own make_valid() would just restructure them into a MultiPolygon/simplified shape the same way it already does for the other examples. What matters here is the disagreement itself, not the fix.

What about relationships between geometries?

A geometry can be perfectly valid on its own and still be wrong in relation to other geometries: two valid polygons overlapping when they shouldn’t, a gap between areas that should touch. That’s a different problem: topology. It’s covered in Topology Errors in a GeoJSON.