I'm using Python 3.12 and I would like to return polygon.exterior.coords
as a type of list[tuple[float, float]]
so that I can correctly enforce typing later in the program. I'm wondering if there is a more elegant solution:
from shapely import Polygon
polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
data = polygon.exterior.coords
# This raises a run time error: IndexError: tuple index out of range
data[0][2]
I would like to do:
data: list[tuple[float, float]] = list(polygon.exterior.coords)
# This now correctly shows a type hint error: "Index 2 is out of range for type tuple[float, float]"
data[0][2]
However the line data: list[tuple[float, float]] = list(polygon.exterior.coords)
then shows a type hint error of:
Type "list[tuple[float, ...]]" is not assignable to declared type "list[tuple[float, float]]"
"list[tuple[float, ...]]" is not assignable to "list[tuple[float, float]]"
Type parameter "_T@list" is invariant, but "tuple[float, ...]" is not the same as "tuple[float, float]"
Consider switching from "list" to "Sequence" which is covariant
I can solve this by using this code:
data = type_safe_list(polygon.exterior.coords)
def type_safe_list(geometry: Polygon) -> list[tuple[float, float]]:
coords: list[tuple[float, float]] = []
for point in clipped_geometry.exterior.coords:
x, y = point
coords.append((x, y))
return coords
But perhaps there's a more elegant solution that doesn't incur a run time penalty? (not that performance is currently an issue for this part of the code). Also I'm actually trying to handle other types like MultiPolygon so the actual implementation I currently have is:
def type_safe_list(geometry: Polygon | MultiPolygon) -> list[tuple[float, float]] | list[list[tuple[float, float]]]:
if isinstance(geometry, Polygon):
coords: list[tuple[float, float]] = []
for point in geometry.exterior.coords:
x, y = point
coords.append((x, y))
return coords
elif isinstance(geometry, MultiPolygon):
multi_coords: list[list[tuple[float, float]]] = []
for poly in geometry.geoms:
coords: list[tuple[float, float]] = []
for point in poly.exterior.coords:
x, y = point
coords.append((x, y))
multi_coords.append(coords)
return multi_coords
else:
raise NotImplementedError(f"Unhandled type: {type(geometry)}")
return []
The type of geometry.exterior.coords
is CoordinateSequence
, whose __iter__
only ever returns an interator of tuple[float, ...]
:
class CoordinateSequence:
...
def __iter__(self) -> Iterator[tuple[float, ...]]: ...
There is no way to change this other than editing the stubs of shapely
itself.
Thus, cast()
is probably your best bet:
def type_safe_list(geometry: Polygon) -> list[tuple[float, float]]:
return cast(list[tuple[float, float]], list(geometry.exterior.coords))