Repository: geopandas/geopandas

Issue:
ENH: Function to show layers in a particular file
As far as I can tell, the only way to see the layers in a particular file is to read the file first, e.g.

```python
import geopandas as gpd

# read the GeoPackage file
gdf = gpd.read_file('your_geopackage_file.gpkg')

# list the layer names
layer_names = list(gdf.name)
print(layer_names)
```

Would it be possible/useful to add a function that takes the file name as the first argument and returns the layers? This from the `sf` package could provide some inspiration: https://r-spatial.github.io/sf/reference/st_layers.html

Apologies in the like event I've missed something, fairly new to this space!


Discussion:
You can use either `pyogrio.list_layers(path)` or `fiona.listlayers(path)`. The code above will not work for that as `gdf` is only a single layer of that gpkg (the first one, unless specified).

We have discussed exposing this on geopandas level but want probably wait for when `pyogrio` becomes the default engine and expose pyogrio's functionality. While it is conceptually the same as fiona's, it returns a slightly different objects.
Thanks for the detailed response, makes sense: you can get the layers but need to use the data I/O engine because the output varies depending on which one use. Logical to add this enhancement after `pyorgrio` becomes default. Will take a look at the roadmap to get a handle on when that is. Learning as I go!

Probably early 2024.
We could also already add it earlier (if someone works on it) and provide a small shim for fiona? I think using the existing fiona APIs, it should be much work to have something that returns the same (getting the geometry type from `meta` dictionary).
I just worked with geopackage that had multiple layers, of which I didn't expect multiple layers in there at first, giving me unexpected data. While in the end I figured it out, I think some improvements in handeling geopackages with multiple layers are certainly possible.

A function that lists layer names would be useful. I think `read_file()` could also be improved when handeling multiple layers. The default behavior is to just silently load only the first layer it encounters. Something more explicit could be done there, for example:
1. It could require the `layer` keyword argument to be specified when loading a geopackage with multiple layers
2. It could warn that there are multiple layers and it only loads the first, if the `layer` argument isn't provided.

If you agree on how to explicitly handle multiple layers, I would be willing to write an PR for it.
> 2\. It could warn that there are multiple layers and it only loads the first, if the `layer` argument isn't provided.

That's actually what was just changed in pyogrio, see https://github.com/geopandas/pyogrio/issues/355 / https://github.com/geopandas/pyogrio/pull/362

That's great to hear, thanks!



Patch:
diff --git a/doc/source/docs/reference/io.rst b/doc/source/docs/reference/io.rst
index f5d77aed23..8c5a8ac9c2 100644
--- a/doc/source/docs/reference/io.rst
+++ b/doc/source/docs/reference/io.rst
@@ -8,6 +8,7 @@ GIS vector files
 .. autosummary::
    :toctree: api/
 
+   list_layers
    read_file
    GeoDataFrame.to_file
 
diff --git a/doc/source/docs/user_guide/io.rst b/doc/source/docs/user_guide/io.rst
index 73f613c7ca..2e659a399d 100644
--- a/doc/source/docs/user_guide/io.rst
+++ b/doc/source/docs/user_guide/io.rst
@@ -32,6 +32,9 @@ the ``layer`` keyword::
 
     countries_gdf = geopandas.read_file("package.gpkg", layer='countries')
 
+If you have a file with multiple layers, you can list them using
+:func:`geopandas.list_layers`. Note that this function requires Pyogrio.
+
 GeoPandas can also load resources directly from
 a web URL, for example for GeoJSON files from `geojson.xyz <http://geojson.xyz/>`_::
 
diff --git a/geopandas/__init__.py b/geopandas/__init__.py
index bbe1b445cd..42994e56b0 100644
--- a/geopandas/__init__.py
+++ b/geopandas/__init__.py
@@ -5,6 +5,7 @@
 from geopandas.array import points_from_xy
 
 from geopandas.io.file import _read_file as read_file
+from geopandas.io.file import _list_layers as list_layers
 from geopandas.io.arrow import _read_parquet as read_parquet
 from geopandas.io.arrow import _read_feather as read_feather
 from geopandas.io.sql import _read_postgis as read_postgis
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index f4d8262b95..aff368574e 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -796,3 +796,34 @@ def _geometry_types(df):
         geom_types = geom_types[0]
 
     return geom_types
+
+
+def _list_layers(filename) -> pd.DataFrame:
+    """List layers available in a file.
+
+    Provides an overview of layers available in a file or URL together with their
+    geometry types. When supported by the data source, this includes both spatial and
+    non-spatial layers. Non-spatial layers are indicated by the ``"geometry_type"``
+    column being ``None``. GeoPandas will not read such layers but they can be read into
+    a pd.DataFrame using :func:`pyogrio.read_dataframe`.
+
+    Parameters
+    ----------
+    filename : str, path object or file-like object
+        Either the absolute or relative path to the file or URL to
+        be opened, or any object with a read() method (such as an open file
+        or StringIO)
+
+    Returns
+    -------
+    pandas.DataFrame
+        A DataFrame with columns "name" and "geometry_type" and one row per layer.
+    """
+    _import_pyogrio()
+    _check_pyogrio("list_layers")
+
+    import pyogrio
+
+    return pd.DataFrame(
+        pyogrio.list_layers(filename), columns=["name", "geometry_type"]
+    )


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index cd1475fd9c..efd09ad5cc 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -1304,3 +1304,24 @@ def test_option_io_engine(nybb_filename):
     finally:
         fiona.supported_drivers["ESRI Shapefile"] = orig
         geopandas.options.io_engine = None
+
+
+@PYOGRIO_MARK
+def test_list_layers(df_points, tmpdir):
+    tempfilename = os.path.join(str(tmpdir), "dataset.gpkg")
+    df_points.to_file(tempfilename, layer="original")
+    df_points.set_geometry(df_points.buffer(1)).to_file(tempfilename, layer="buffered")
+    df_points.set_geometry(df_points.buffer(2).boundary).to_file(
+        tempfilename, layer="boundary"
+    )
+    pyogrio.write_dataframe(
+        df_points[["value1", "value2"]], tempfilename, layer="non-spatial"
+    )
+    layers = geopandas.list_layers(tempfilename)
+    expected = pd.DataFrame(
+        {
+            "name": ["original", "buffered", "boundary", "non-spatial"],
+            "geometry_type": ["Point", "Polygon", "LineString", None],
+        }
+    )
+    assert_frame_equal(layers, expected)