pythonpython-typingshapely

Elegantly handling python type hint for Shapely polygon.exterior.coords


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 []

Solution

  • 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))