shapely review
Shapely wraps GEOS for planar geometry in Python. Points, lines, polygons, and collections support predicates, overlays, buffering, measurement, simplification, repair, serialization, and spatial indexing. Call methods on one geometry or use module-level NumPy ufuncs over arrays; GEOS performs the heavy work and Shapely releases the GIL for many operations. Version 2.1.2 adds wheels for Python 3.14 and keeps GEOS 3.13.1 bundled in those wheels. The package does not track coordinate reference systems or provide dataset file I/O. Distances and areas use the numeric units of the input coordinates, so longitude and latitude need projection or geodesic handling elsewhere.
Use Shapely for in-memory planar geometry and vectorized GEOS operations. Reach for GeoPandas when geometry belongs in a table, and use pyproj before trusting measurements derived from geographic coordinates.
We installed it
| Install | ✓ · 1s | 2 packages on disk · 68 MB |
| Import | ✓ | import shapely in 0.38s · compiled extensions · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does shapely install cleanly?
Yes. In a fresh container with an empty cache, pip install shapely finished in 1 seconds, leaving 2 packages and 68 MB on disk. pip-audit reported no known vulnerabilities.
What does shapely need to run?
Python >=3.10, and a platform wheel with compiled extensions. In our run import shapely succeeded in 0.38s.
shapely or geopandas: which should you use?
geopandas: Use it when geometry rows also need attribute columns, CRS metadata, file I/O, and spatial joins. Use Shapely for in-memory planar geometry and vectorized GEOS operations.
When should you not use shapely?
You need geodesic distance, area, or buffering on longitude and latitude. Shapely treats those coordinates as a flat Cartesian plane and knows no CRS.
Use it if
- Your code needs GEOS predicates and overlay operations on points, lines, polygons, or geometry collections.
- Arrays of geometries should use broadcasting ufuncs instead of a Python loop around scalar methods.
- Candidate searches need an immutable STRtree before an exact contains, intersects, nearest, or distance test.
- A library must exchange geometry objects with GeoPandas, pyogrio, PostGIS adapters, or tools using the geo interface.
- You need geodesic distance, area, or buffering on longitude and latitude. Shapely treats those coordinates as a flat Cartesian plane and knows no CRS.
- The main task is reading or writing Shapefile, GeoPackage, FlatGeobuf, or whole GeoJSON datasets. Shapely only serializes individual geometries and basic geometry arrays.
- Rows need attributes, CRS metadata, spatial joins, and file I/O. GeoPandas supplies that table layer on top of Shapely objects.
- The dataset is larger than process memory or belongs beside database filtering. A spatial database or analytical engine avoids materializing every geometry as a Python-owned object.
- Your type checker requires package-supplied typing markers. Our 2.1.2 installation did not include py.typed, despite the library's broad runtime API.
Setup reality
We installed Shapely 2.1.2 in a clean Python 3.12 Bookworm container. Installation succeeded in 1 second, left 2 packages, and used 68 MB. Its metadata declares 9 direct dependencies, requires Python 3.10 or newer, and the distribution contains compiled .so extensions. It uses the BSD 3-Clause license and did not ship py.typed in our environment. import shapely completed in 0.38 seconds. pip-audit reported 0 known vulnerabilities. The large disk footprint relative to the package count comes from native geometry code and NumPy.
Official wheels bundle GEOS, and 2.1.2 added Python 3.14 wheels built with GEOS 3.13.1. Check shapely.geos_version when a function has a minimum GEOS requirement. Source installs need a compatible GEOS development library and compiler. Mixing a pip wheel with conda or system geospatial libraries can load different GEOS copies into one process, so keep Shapely, GeoPandas, GDAL-facing packages, and their native dependencies on one packaging channel when possible.
Shapely never assigns a CRS or changes units. Project longitude and latitude with pyproj before planar measurement, using an appropriate local or equal-area CRS for the question. Geometry constructors also allow invalid topology; call is_valid and inspect explain_validity at ingestion boundaries. make_valid may return a MultiPolygon or GeometryCollection, which downstream schemas must accept. Multi-part geometries are immutable in 2.x and expose parts through .geoms. STRtree indexes the geometry array it was built from and returns indices, so keep that source array stable for the tree's lifetime.
Patterns
Construct common geometry types create-geometries
from shapely import Point, LineString, Polygon, box, MultiPolygon
p = Point(2.0, 3.0)
line = LineString([(0, 0), (1, 1), (2, 0)])
square = box(0, 0, 10, 10) # minx, miny, maxx, maxy
ring_with_hole = Polygon(
shell=[(0, 0), (0, 10), (10, 10), (10, 0)],
holes=[[(2, 2), (2, 4), (4, 4), (4, 2)]],
)
print(square.area, ring_with_hole.area)Geometry objects are immutable in 2.x. Coordinates carry no CRS or unit metadata, and polygon holes need rings that make sense within the shell.
Choose the right spatial predicate spatial-predicates
square.contains(p) # strictly inside, boundary excluded
square.covers(p) # inside or on the boundary
square.intersects(line)
square.touches(line) # boundaries meet, interiors do not
square.disjoint(line)
# full DE-9IM when the named predicates are not precise enough
square.relate(line) # e.g. '1010F0212'
square.relate_pattern(line, 'T*F**F***')contains excludes a geometry that lies only on the boundary, while covers includes it. Use relate patterns when a named predicate does not express the required topology.
Broadcast operations over geometry arrays vectorized-operations
import numpy as np
import shapely
points = shapely.points(np.random.rand(1_000_000, 2) * 100)
region = shapely.box(10, 10, 20, 20)
inside = shapely.contains(region, points) # bool array, loop in C
areas = shapely.area(shapely.buffer(points[:1000], 0.5))
merged = shapely.union_all(shapely.buffer(points[:1000], 0.5))Module-level functions follow NumPy broadcasting and keep the element loop in C. `shapely.points` expects coordinate arrays, which avoids constructing each Point in Python.
Query an STRtree by index spatial-index
from shapely import STRtree
tree = STRtree(parcels) # array of polygons
# which parcels intersect each building?
building_idx, parcel_idx = tree.query(buildings, predicate="intersects")
# nearest parcel to each building, with distance
idx, dist = tree.query_nearest(buildings, return_distance=True)Results are integer positions into the input arrays. A query without a predicate returns bounding-box candidates; provide a predicate for exact GEOS filtering.
Prepare a repeated predicate target prepare-geometry
import shapely
shapely.prepare(coastline) # mutates in place, builds an internal index
hits = shapely.contains(coastline, points)
shapely.destroy_prepared(coastline)prepare changes internal state in place and helps supported predicates that reuse the same left-hand geometry. It does not accelerate overlay operations.
Validate and repair topology repair-invalid-geometry
from shapely import is_valid, make_valid, set_precision
from shapely.validation import explain_validity
bowtie = Polygon([(0, 0), (2, 2), (2, 0), (0, 2)])
print(is_valid(bowtie)) # False
print(explain_validity(bowtie)) # 'Self-intersection[1 1]'
fixed = make_valid(bowtie) # returns a MultiPolygon here
snapped = set_precision(fixed, 0.001)make_valid may return a different geometry family, including a collection. Validate the repaired type before storage; set_precision changes coordinates and should use a domain-appropriate grid.
Serialize individual geometries read-write-formats
import shapely
wkt = shapely.to_wkt(square, rounding_precision=6)
wkb = shapely.to_wkb(square, hex=True)
gj = shapely.to_geojson(square)
back = shapely.from_wkt(wkt)
from_db = shapely.from_wkb(row["geom"])
lenient = shapely.from_wkt(bad_text, on_invalid="warn")These functions handle geometry values rather than datasets. Set WKT precision deliberately, and choose an on_invalid policy before parsing untrusted bulk input.
Project before planar measurement reproject-coordinates
from pyproj import Transformer
from shapely.ops import transform
# WGS84 lon/lat to Web Mercator metres
to_metres = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
projected = transform(to_metres.transform, lonlat_geom)
print(projected.length) # metres, not degreesalways_xy keeps longitude in the x position expected by Shapely. EPSG:3857 is only an illustration; choose a local or equal-area CRS when accuracy matters.
Simplify or densify in coordinate units measure-and-simplify
import shapely
shapely.length(road)
shapely.area(parcel)
shapely.distance(parcel, well)
small = shapely.simplify(coastline, tolerance=50, preserve_topology=True)
dense = shapely.segmentize(road, max_segment_length=10)
hull = shapely.concave_hull(points_multi, ratio=0.3) # GEOS 3.11+Every distance argument uses the current coordinate units. Topology-preserving simplification costs more but avoids some invalid outputs; segmentize adds vertices before curved reprojection.
Access multi-part members and coordinates access-parts-and-coordinates
for poly in multi.geoms: # NOT: for poly in multi
print(poly.exterior.coords[:])
for hole in poly.interiors:
print(len(hole.coords))
import shapely
coords = shapely.get_coordinates(geoms_array) # (N, 2) float array
parts = shapely.get_parts(geoms_array) # explode multipartsUse `.geoms` for multi-part members in 2.x. `get_coordinates` flattens input boundaries, so request source indices when coordinates must be mapped back to their geometries.
Apply overlay and buffer operations overlay-operations
import shapely
union = shapely.union_all(parcels) # one call, not a fold
clipped = shapely.intersection(parcel, flood_zone)
remaining = shapely.difference(parcel, easement)
ring = shapely.buffer(road, 25).difference(shapely.buffer(road, 15))
edge = shapely.buffer(site, -5, join_style="mitre") # negative shrinksUse union_all for a collection rather than folding binary unions in Python. Negative buffers can produce empty results, and overlays may return collections when dimensions mix.
Locate positions along a line linear-referencing
from shapely.ops import nearest_points, substring, split
d = road.project(incident) # distance along the line
snapped = road.interpolate(d) # point at that distance
half = road.interpolate(0.5, normalized=True)
a, b = nearest_points(road, incident)
segment = substring(road, 100, 250)
pieces = split(road, crossing_line)project returns distance along the line, while interpolate returns a point at that distance. Normalized mode uses a fraction; split still requires an actual intersection.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| geopandas | PyPI | Use it when geometry rows also need attribute columns, CRS metadata, file I/O, and spatial joins. |
| pyproj | PyPI | Use it for CRS transformations and ellipsoidal distance or area calculations before or alongside planar operations. |
| pyclipper | PyPI | Use it for integer-coordinate polygon clipping and offsetting when the Clipper algorithm matches the domain. |
| trimesh | PyPI | Use it for three-dimensional meshes, scenes, and solids; Shapely's operations are planar. |
More data guides
numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

