Repository: shapely/shapely

Issue:
Type of `x`, `y` and `z` values of a Point are now `numpy.float64`
With NumPy's 2.0 release, they've [changed the representation of their scalars](https://numpy.org/devdocs/release/2.0.0-notes.html#representation-of-numpy-scalars-changed). This has caused the `x`, `y`, and `z` attribute of a Point to no longe be [Python's built-in float type](https://docs.python.org/3/library/functions.html#float), but [NumPy's float64 type](https://numpy.org/doc/stable/reference/arrays.scalars.html#numpy.float64) instead.

```sh
>>> from shapely import Point
>>> point = Point(1, 2)
>>> type(point.x)
<class 'numpy.float64'>
```

However, according to Shapely's documentation of [Point](https://shapely.readthedocs.io/en/stable/reference/shapely.Point.html#shapely.Point), these attributes should be the normal `float` type instead. 

---

This change caused issues in our project because we were passing a Point's `x` and `y` value to our ORM (SQLAlchemy), which was unable to convert a `numpy.float64` to a database value.

What is the plan for Shapely going forward? I think it would make sense to have Shapely return Python built-in types, but if you're planning to take the NumPy approach, then the documentation should be updated.


Discussion:
Agree this is a minor nuisance, it would be nice to have this fixed for the next minor release of the 2.0.x series.

I'll need to investigate a bit deeper, as there are subtle nuances between some properties and top-level functions, e.g.:
```pycon
>>> import shapely
>>> point = shapely.from_wkt("point(1 2)")
>>> poly = point.buffer(1.0)
>>> shapely.area(poly)
np.float64(3.136548490545939)
>>> poly.area
3.136548490545939
```
ideally all scalar numbers would be `float` built-in types.
> there are subtle nuances between some properties and top-level functions, e.g.:

Yes, that's because we actually do try to ensure to return Python scalars for most of the attributes/methods on the geometry objects. For example for `area` we explicitly convert the result to a float:

https://github.com/shapely/shapely/blob/bada70fbe79f61ea98dc735d630d61a082cd7474/shapely/geometry/base.py#L327-L330

We should do that consistently, and so should probably also do that for the Point `x`/`y`/`z`/`m` attributes indeed.




> This has caused the `x`, `y`, and `z` attribute of a Point to no longe be [Python's built-in float type](https://docs.python.org/3/library/functions.html#float), but [NumPy's float64 type](https://numpy.org/doc/stable/reference/arrays.scalars.html#numpy.float64) instead.

FWIW, this did not actually change: also with NumPy 1.26, the return value was a `numpy.float64` scalar. But as you mentioned, the `repr` of the numpy scalars changed in NumPy 2.0.0, making it now much more obvious that the return value is indeed a numpy scalar and not a Python float. 
So I am not entirely sure why other libraries like SQLAlchemy started to fail on numpy scalars (maybe there was another change to how scalars behave in NumPy 2.0), but so we already have been returning such numpy scalars in 2.0.x for more than a year.

That said, as mentioned above, we should certainly make this consistent and have all geometry object attributes/methods return python scalars where possible.



Patch:
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index 8c1660ed1..045c79676 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -85,19 +85,19 @@ def __new__(self, *args):
     @property
     def x(self):
         """Return x coordinate."""
-        return shapely.get_x(self)
+        return float(shapely.get_x(self))
 
     @property
     def y(self):
         """Return y coordinate."""
-        return shapely.get_y(self)
+        return float(shapely.get_y(self))
 
     @property
     def z(self):
         """Return z coordinate."""
         if not shapely.has_z(self):
             raise DimensionError("This point has no z coordinate.")
-        return shapely.get_z(self)
+        return float(shapely.get_z(self))
 
     @property
     def m(self):
@@ -108,7 +108,7 @@ def m(self):
         """
         if not shapely.has_m(self):
             raise DimensionError("This point has no m coordinate.")
-        return shapely.get_m(self)
+        return float(shapely.get_m(self))
 
     @property
     def __geo_interface__(self):


Test patch:
diff --git a/shapely/tests/geometry/test_point.py b/shapely/tests/geometry/test_point.py
index 505459c04..bb5811a45 100644
--- a/shapely/tests/geometry/test_point.py
+++ b/shapely/tests/geometry/test_point.py
@@ -101,7 +101,9 @@ def test_point(self):
         # Test XY point
         p = Point(1.0, 2.0)
         assert p.x == 1.0
+        assert type(p.x) is float
         assert p.y == 2.0
+        assert type(p.y) is float
         assert p.coords[:] == [(1.0, 2.0)]
         assert str(p) == p.wkt
         assert p.has_z is False
@@ -121,6 +123,7 @@ def test_point(self):
         assert str(p) == p.wkt
         assert p.has_z is True
         assert p.z == 3.0
+        assert type(p.z) is float
         if geos_version >= (3, 12, 0):
             assert p.has_m is False
             with pytest.raises(DimensionError):