flowchart LR
A["🗺️ Looks fine on the map"] --> B{"Validity check"}
B --> C["❌ INVALID: why?"]
Geometry validity: find and understand invalid geometries
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?
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.
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_validR
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 onsf::sf_use_s2().
For the diagnosis above the difference doesn’t show. It becomes concrete in the repair, below:
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.
Gallery of geometric errors
GEOS reports one of a small, closed set of validity failures, not a free-text description: an exact list, verified against TopologyValidationError.cpp in the GEOS source itself. The diagnose cells above match every message to one of these names, shown in the “category” column; a message that matches none of them shows up as “Other” rather than being forced into the closest one.
| Category | What happens | Example below |
|---|---|---|
| Self-intersection | The boundary crosses itself | ✓ |
| Ring Self-intersection | The boundary touches itself at a point, without crossing | ✓ |
| Hole lies outside shell | A hole sits entirely outside the outer boundary* | ✓ |
| Holes are nested | A hole contains its own shell* | ✓ |
| Interior is disconnected | A hole splits the interior into pieces that don’t connect | — |
| Nested shells | One polygon’s shell sits entirely inside another’s, in a MultiPolygon |
— |
| Too few points | A ring has fewer than the 4 points a closed ring needs | — |
| Invalid Coordinate | A coordinate is NaN or infinite |
— |
| Repeated Point | The same point twice in a row | — |
| Duplicate Rings | Two rings describe the exact same boundary | — |
| Ring is not closed | A ring’s first and last point don’t match | — |
* Valid under R’s default S2 engine, invalid under GEOS: see Where R and Python actually disagree, past Repair.
Of the rows without an example, “Interior is disconnected” belongs to the same family as the crossing cases below, “Nested shells” needs two full polygons to set up, and “Too few points” and “Invalid Coordinate” are rarely produced by a real file. The last three are worth being honest about: they’re real GEOS categories the classifier still recognizes if they ever show up, but none of them could be reproduced through an actual upload here. As of GEOS 3.11+, a repeated point is reported as “Too few points” instead (verified live, not assumed from the changelog). Every duplicate-ring construction tried got caught as “Self-intersection” first, before that check ever ran. And both GeoJSON and Shapefile close every ring at the file format level, before geopandas or sf ever see it, so an unclosed ring never reaches GEOS.
Each example below loads its own data into the diagnosis cell pair above. No repair here, that’s in the Repair section further down.
Self-intersection (“bowtie”)
The polygon’s boundary crosses itself, forming a bowtie shape instead of a single closed area. It’s the most common case in practice, often the result of imprecise manual digitizing.
Scroll to the Repair section at the bottom of the page to see how it’s fixed.
A hole that crosses the boundary
A “hole” (inner ring, e.g. a lake inside a land polygon) must lie entirely within the outer boundary. Here it doesn’t: one corner sticks out past the shell’s own edge.
This is “Self-intersection”, not “Hole lies outside shell”. Load the example and check the category column: GEOS classifies it as a self-intersection, because the hole’s ring literally crosses the shell’s ring at that corner: a crossing is a crossing, whichever ring it happens between. GEOS’s own “Hole lies outside shell” category needs the hole to be entirely disjoint from the shell, with no crossing at all. That’s the next example.
Scroll to the Repair section at the bottom of the page to see how it’s fixed.
A hole with no crossing at all
Same rule as above (a hole must lie entirely within the shell), but this time nothing crosses: the “hole” ring sits well clear of the shell’s own boundary, disjoint from it. This is what GEOS’s own “Hole lies outside shell” category actually needs. A hole that merely crosses the boundary (previous example) is a self-intersection instead, a different failure with a different name.
Load this example and run it in both languages: R’s “valid” column shows ✓ where Python’s shows ✗. That’s not a quirk of this one case; see Where R and Python actually disagree near the end of the page for why.
Scroll to the Repair section at the bottom of the page to see how it’s fixed.
Ring touching itself (hourglass)
The polygon’s boundary touches itself at one point, without crossing, effectively splitting the shape into two lobes joined by a single vertex: not cleanly connected, so invalid.
Scroll to the Repair section at the bottom of the page to see how it’s fixed.
Overlapping holes
If a polygon has more than one hole, they can’t overlap each other: two intersecting holes create an ambiguous area (inside or outside the polygon?) that the model doesn’t allow.
Scroll to the Repair section below to see how it’s fixed.
Holes are nested
A hole with a smaller shape sitting inside that hole, like an island in a lake inside a larger landmass. The Simple Features model doesn’t allow this as one Polygon: a hole containing its own shell is supposed to be two separate Polygons (the landmass-with-lake, and the island) combined into a MultiPolygon, not nested rings in one.
Load the example and run it in both languages: Python marks it ✗ with category “Holes are nested”. R’s default marks it ✓, valid, so it has no category at all. That’s not a wording difference; see Where R and Python actually disagree near the end of the page for why, and why it’s not shown in the Repair section below (there’s nothing to repair under R’s default, since it isn’t flagged invalid there to begin with).
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.