Repository: geopandas/geopandas

Issue:
Slight change in GeoDataFrame constructor behavior related to geometry column detection
In v1.0.1 this worked:
```python
gdf = gpd.GeoDataFrame(
    data=[[np.nan, np.nan]],
    columns=["geometry", "something"],
    crs="EPSG:4326",
)
```
silently converting the single value for geometry from NaN to `None`.

In v1.1.0, it fails with:

`ValueError: Assigning CRS to a GeoDataFrame without a geometry column is not supported. Supply geometry using the 'geometry=' keyword argument, or by providing a DataFrame with column name 'geometry'`

And indeed, even though we already _have_ a column named 'geometry', it does work if you add `geometry="geometry"`, silently converting the NaN to `None`.

It also doesn't complain if NaN isn't the only value, for example:
```python
gdf = gpd.GeoDataFrame(
    {
        "geometry": [np.nan, Point(0, 0)],
        "something": [np.nan, np.nan],
    },
    crs="EPSG:4326",
)
```
(again the NaN becomes `None`)

Hints from issue comments:
@martinfleis: Thanks for the report. It is bit of a corner case as np.nan is technically a float not a geometry object or None. But given this works, the snippet above should likely too.

```py
gdf = gpd.GeoDataFrame(
    data=[[np.nan]],
    columns=["something"],
    geometry=[np.nan],
    crs="EPSG:4326",
)
```

Fix is coming up.@zmoon: Thanks for the swift response @martinfleis , I agree it is an unusual case, I wouldn't think it would come up often. I only noticed because I was creating some data in a test that way.

Looking at the PR and experimenting a bit more, I would note that in v1.0.1 it also works for longer columns where `np.nan` is the only unique value, not just size-one.
```python
gdf = gpd.GeoDataFrame(
    data=np.full((10, 2), np.nan),
    columns=["geometry", "something"],
    crs="EPSG:4326",
)
```

Same with a mix of null types:
```python
gdf = gpd.GeoDataFrame(
    {
        "geometry": [np.nan, None, pd.NA],
        "something": [np.nan, np.nan, np.nan],
    },
    crs="EPSG:4326",
)
```@martinfleis: Thanks. I have updated the PR. I am still not sure if this is a behaviour we should encourage but given our geometry sanitisation deals with it in other cases, no need to disallow it here.

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3cc8855..56554d4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## Version 1.1.1
+
+- Fix regression in the GeoDataFrame constructor when np.nan is given as an only geometry (#3591).
+
 ## Version 1.1.0 (June 1, 2025)
 
 Notes on dependencies:
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 1a607cbb..243d9875 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -211,6 +211,8 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
                 or geom_dtype == "object"
                 # special case for geometry = [], has float dtype
                 or (len(self) == 0 and geom_dtype == "float")
+                # special case for geometry = [np.nan]
+                or ((not self.empty) and self["geometry"].isna().all())
             ):
                 try:
                     self["geometry"] = _ensure_geometry(self["geometry"].values, crs)


Test patch:
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index a9b7ac04..61eac57b 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -1513,6 +1513,32 @@ class TestConstructor:
         res = GeoDataFrame(df, geometry=Fruit.pear)
         assert res.active_geometry_name == Fruit.pear
 
+    def test_geometry_nan_scalar(self):
+        gdf = GeoDataFrame(
+            data=[[np.nan, np.nan]],
+            columns=["geometry", "something"],
+            crs="EPSG:4326",
+        )
+        assert gdf.shape == (1, 2)
+        assert gdf.active_geometry_name == "geometry"
+        assert gdf.geometry[0] is None
+        if compat.HAS_PYPROJ:
+            assert gdf.crs == "EPSG:4326"
+
+    def test_geometry_nan_array(self):
+        gdf = GeoDataFrame(
+            {
+                "geometry": [np.nan, None, pd.NA],
+                "something": [np.nan, np.nan, np.nan],
+            },
+            crs="EPSG:4326",
+        )
+        assert gdf.shape == (3, 2)
+        assert gdf.active_geometry_name == "geometry"
+        assert gdf.geometry.isna().all()
+        if compat.HAS_PYPROJ:
+            assert gdf.crs == "EPSG:4326"
+
 
 @pytest.mark.skipif(not compat.HAS_PYPROJ, reason="pyproj not available")
 def test_geodataframe_crs():