Shapely TopologyException: Found Non-Noded Intersection (How to Fix)
Problem statement
A common GIS problem is an overlay or geometry operation that fails partway through with a topology error from GEOS, the geometry engine behind Shapely and GeoPandas.
The error usually looks like one of these:
shapely.errors.GEOSException: TopologyException: found non-noded intersection between LINESTRING ... at ...
shapely.errors.GEOSException: TopologyException: side location conflict at ...
It often appears when you run:
gpd.overlay(a, b, how="intersection")geometry.intersection(other)union_all()dissolve()clip()
The usual causes are:
- invalid or self-intersecting polygons in one of the inputs
- slivers and tiny artifacts left over from earlier processing
- nearly coincident edges that GEOS cannot resolve at full coordinate precision
- mixing layers with slightly different vertex precision after reprojection
The practical fix is to detect invalid geometries, repair them, optionally reduce coordinate precision, and then retry the operation.
Quick answer
The shortest reliable workflow to get past a TopologyException in GeoPandas is:
- check both inputs with
.is_valid - repair invalid features with
make_valid() - if the error persists, reduce precision with
shapely.set_precision() - retry the overlay, intersection, or dissolve
import geopandas as gpd
from shapely import make_valid, set_precision
a = gpd.read_file("data/parcels.shp")
b = gpd.read_file("data/zoning.shp")
# 1. repair invalid geometries in both layers
a["geometry"] = a.geometry.make_valid()
b["geometry"] = b.geometry.make_valid()
# 2. optional: snap coordinates to a grid to remove precision noise
a["geometry"] = set_precision(a.geometry.values, grid_size=0.001)
b["geometry"] = set_precision(b.geometry.values, grid_size=0.001)
# 3. retry the operation
result = gpd.overlay(a, b, how="intersection")
Repair with make_valid() first. Use set_precision() only if the error survives the repair step. Use buffer(0) as a fallback when make_valid() is unavailable.
Repair order
Step-by-step solution
Reproduce the error and read the message
The error message includes the coordinate where GEOS gave up. That coordinate is a clue, not the whole answer, but it tells you roughly where the problem geometry sits.
import geopandas as gpd
a = gpd.read_file("data/parcels.shp")
b = gpd.read_file("data/zoning.shp")
result = gpd.overlay(a, b, how="intersection")
If this raises TopologyException: found non-noded intersection between ..., the rest of these steps will help you fix it.
Check validity in both input layers
The most common trigger is an invalid geometry in one of the inputs. Check both layers, not just one.
print("Invalid in a:", (~a.geometry.is_valid).sum())
print("Invalid in b:", (~b.geometry.is_valid).sum())
Even a single invalid polygon in either layer can stop the whole overlay.
Diagnose the reason with explain_validity()
Finding the count is useful, but the reason tells you what kind of defect you are dealing with.
from shapely.validation import explain_validity
invalid_a = a[~a.geometry.is_valid].copy()
invalid_a["reason"] = invalid_a.geometry.apply(explain_validity)
print(invalid_a[["reason"]].head(10))
Typical messages include Self-intersection, Ring Self-intersection, and Too few points in geometry component. A self-intersection is the classic source of a non-noded intersection error.
Repair geometries with make_valid()
The preferred repair is make_valid() from Shapely 2.x. Apply it to both layers.
from shapely import make_valid
a["geometry"] = a.geometry.make_valid()
b["geometry"] = b.geometry.make_valid()
print("Invalid in a after repair:", (~a.geometry.is_valid).sum())
print("Invalid in b after repair:", (~b.geometry.is_valid).sum())
gdf.geometry.make_valid() is the GeoSeries method; make_valid(geom) from shapely works on a single geometry. Either is fine.
Retry the operation
After repair, run the same operation again.
result = gpd.overlay(a, b, how="intersection")
print(len(result))
Many TopologyException cases are solved here. If the error is gone, you are done.
Reduce precision with set_precision() if the error persists
If repair alone does not help, the problem is usually precision: two edges that are almost but not exactly coincident. Snapping coordinates to a grid forces GEOS to treat near-equal points as equal.
from shapely import set_precision
# grid_size is in CRS units. For a projected CRS in meters, 0.001 is 1 mm.
a["geometry"] = set_precision(a.geometry.values, grid_size=0.001)
b["geometry"] = set_precision(b.geometry.values, grid_size=0.001)
result = gpd.overlay(a, b, how="intersection")
Pick grid_size based on your CRS units and the precision you can afford to lose. Start small. A grid that is too coarse will collapse real detail.
Use buffer(0) as a fallback
If make_valid() is not available in your environment, buffer(0) can repair simple polygon self-intersections.
a["geometry"] = a.geometry.buffer(0)
b["geometry"] = b.geometry.buffer(0)
result = gpd.overlay(a, b, how="intersection")
Use this carefully. buffer(0) can drop slivers or change shape, and it only works on polygons. It is not the best default when make_valid() is available.
Code examples
Example 1: Repair both layers and retry an overlay
import geopandas as gpd
from shapely import make_valid
a = gpd.read_file("data/parcels.shp")
b = gpd.read_file("data/zoning.shp")
a["geometry"] = a.geometry.make_valid()
b["geometry"] = b.geometry.make_valid()
result = gpd.overlay(a, b, how="intersection")
print("Output features:", len(result))
Example 2: Fix a self-intersection with buffer(0) before union_all()
import geopandas as gpd
gdf = gpd.read_file("data/districts.shp")
invalid_mask = ~gdf.geometry.is_valid
gdf.loc[invalid_mask, "geometry"] = gdf.loc[invalid_mask, "geometry"].buffer(0)
merged = gdf.geometry.union_all()
print(merged.geom_type)
union_all() replaces the deprecated unary_union. Repairing first avoids a TopologyException during the merge.
Example 3: Snap coordinates with set_precision() before a dissolve
import geopandas as gpd
from shapely import set_precision
gdf = gpd.read_file("data/landuse.shp")
gdf["geometry"] = gdf.geometry.make_valid()
gdf["geometry"] = set_precision(gdf.geometry.values, grid_size=0.001)
dissolved = gdf.dissolve(by="landuse_class")
print(dissolved.geometry.geom_type.value_counts())
dissolve() runs a union internally, so it is a common place to hit noding errors. Repair and precision snapping before the dissolve usually clears them.
Example 4: Identify the offending row before a clip
When the error coordinate is not enough, find which feature is invalid so you can inspect or remove it.
import geopandas as gpd
from shapely.validation import explain_validity
gdf = gpd.read_file("data/parcels.shp")
mask = gpd.read_file("data/study_area.shp")
bad = gdf[~gdf.geometry.is_valid].copy()
bad["reason"] = bad.geometry.apply(explain_validity)
print(bad[["reason"]])
# repair, then clip
gdf["geometry"] = gdf.geometry.make_valid()
clipped = gpd.clip(gdf, mask)
print("Clipped features:", len(clipped))
Explanation
"Noding" is the step where GEOS splits every line at the points where it crosses or touches another line. Each of those crossing points becomes a node. Overlay, union, dissolve, and clip all depend on a correctly noded set of edges before they can build the output polygons.
A "non-noded intersection" means GEOS found two segments that cross at a point that was not turned into a node. This happens when geometry is invalid, when edges nearly overlap, or when floating-point coordinates put an intersection a tiny fraction off from where the noding logic expected it. A "side location conflict" is a related symptom: GEOS cannot decide which side of an edge is inside and which is outside, usually because of the same precision or validity problem.
GeoPandas does not do this work itself. It hands the geometries to Shapely, which calls GEOS, so the error and the fix both live at the GEOS level. That is why the repair workflow is always the same regardless of which GeoPandas function raised it:
- make the geometries valid with
make_valid() - if needed, reduce coordinate precision with
set_precision()so near-coincident edges snap together - retry the operation
Reducing precision works because GEOS uses a fixed precision model when a grid size is set. Snapping coordinates to that grid removes the sub-grid noise that produced the unnoded intersection in the first place.
Edge cases or notes
make_valid() can change geometry type
A repaired polygon may come back as a MultiPolygon or a GeometryCollection. If a later step expects polygons only, filter on geom_type before continuing.
polygon_types = ["Polygon", "MultiPolygon"]
gdf = gdf[gdf.geometry.geom_type.isin(polygon_types)].copy()
set_precision() is destructive
Snapping to a grid moves vertices. A grid size that is too large will merge distinct features or collapse thin polygons to nothing. Choose the smallest grid that clears the error.
Precision depends on CRS units
grid_size is expressed in the units of the layer's CRS. In a projected CRS measured in meters, 0.001 is one millimeter. In a geographic CRS measured in degrees, the same number is a much larger distance. Reproject to a projected CRS before snapping if you can.
Both inputs must be repaired
Overlay and clip take two layers. An invalid geometry in either one can cause the error, so always check and repair both, not just the layer you expect to be at fault.
buffer(0) only helps with polygons
It can fix simple polygon self-intersections, but it does nothing useful for lines and can silently delete small parts. Prefer make_valid().
Keep CRS consistent first
Mismatched CRS will not directly cause a noding error, but reprojecting after an overlay is harder than before. Align CRS with to_crs() before running the operation.
Internal links
- How to Fix Invalid Geometries in Python (GeoPandas)
- Overlay Operations in GeoPandas: Union, Intersection, Difference Explained
- How to Simplify Geometry in Python with GeoPandas and Shapely
- How to Dissolve Polygons in Python (GeoPandas)
- How to Clip Spatial Data in Python with GeoPandas
- Shapely Basics: Working with Geometry Objects in Python
FAQ
What does "found non-noded intersection" actually mean?
GEOS found two line segments that cross at a point it did not split into a node, usually because of an invalid geometry or a precision mismatch between near-coincident edges. Repairing geometries and reducing precision resolves most cases.
Will make_valid() always fix the TopologyException?
Often, but not always. If the problem is precision rather than validity, the geometries can be valid and still produce the error. In that case, follow make_valid() with set_precision() and retry.
What grid_size should I pass to set_precision()?
Use a value in your CRS units that is smaller than the detail you need to keep. For a projected CRS in meters, 0.001 (1 mm) is a safe starting point. Increase it only if the error persists.
Should I use buffer(0) or make_valid()?
Use make_valid(). It handles more cases and works on lines as well as polygons. Keep buffer(0) as a fallback for simple polygon self-intersections when make_valid() is not available.
Can I make GeoPandas overlay use the slower but more robust GEOS noder?
Recent GEOS handles most overlay noding internally, but if precision is the issue the practical lever is set_precision() with a fixed grid size, which forces a robust fixed-precision overlay. Apply it to both inputs, then retry the operation rather than changing engine settings.
Why does the TopologyException only appear after I reproject my data?
to_crs() recomputes every coordinate, which can introduce tiny floating-point offsets that turn previously coincident edges into near-coincident ones. Run make_valid() after reprojecting, and if the error persists snap both layers with set_precision() in the new CRS units.
My geometries all report valid but the overlay still fails. What now?
Validity and noding are different problems: two valid polygons can still have edges that are almost-but-not-exactly coincident. Apply set_precision() with a small grid_size in your CRS units to both layers and retry, since the issue is precision rather than invalidity.
How do I isolate the single feature causing the error so I can inspect it?
Use the coordinate in the error message to filter, for example build a small shapely.geometry.Point there and select features whose bounds are near it, or test intersection() row by row in a loop and catch the GEOSException to flag the offending row. Once identified, inspect it with explain_validity() and repair or drop it.
A note: for the centroids file, the assigned filename is how-to-get-polygon-centroids-in-geopandas.md. Corrected block header below.