How to Read and Write GeoPackage Files in Python
On this page
Problem statement
A GeoPackage is one file that can hold many layers, so reading one means knowing which layer you are getting — read_file without layer= quietly takes the first. The same file comes up in:
- opening a GeoPackage exported from QGIS
- reading one layer from a file that contains
roads,parcels, andbuildings - editing attributes or filtering features
- exporting processed data to a new
.gpkg - saving multiple output layers into one file for delivery or automation
This page shows the practical GeoPandas workflow, run on a GeoPackage of Rutland holding exactly those three layers — OpenStreetMap roads and buildings and HM Land Registry's parcels — plus a table with no geometry.
Quick answer
Use GeoPandas to read and write GeoPackage files. If the file has multiple layers, list them first and then load the layer you need.
import geopandas as gpd
print(gpd.list_layers("data/city_data.gpkg"))
# Read one layer from a GeoPackage
gdf = gpd.read_file("data/city_data.gpkg", layer="parcels")
print(len(gdf), "parcels,", gdf.crs)
# Write to a new GeoPackage
gdf.to_file("output/parcels_copy.gpkg", layer="parcels", driver="GPKG")
print(gpd.list_layers("output/parcels_copy.gpkg"))
name geometry_type
0 roads LineString
1 parcels Polygon
2 buildings Polygon
3 road_classes NaN
21253 parcels, EPSG:27700
name geometry_type
0 parcels Polygon
A GeoPackage can store multiple layers in one file, unlike a shapefile — including tables with no geometry at all, which list_layers reports with no geometry type (NaN).
One file, many layers
Read a named layer, inspect, write one or many
Install the required Python packages
You need GeoPandas and a working vector I/O stack. GeoPandas reads and writes files through an engine — pyogrio by default in GeoPandas 1.x, with Fiona as an alternative — which uses GDAL underneath.
pip install geopandas
import pyogrio
print("GeoPandas", gpd.__version__, "| pyogrio", pyogrio.__version__, "| GDAL", pyogrio.__gdal_version_string__)
print("GPKG driver capabilities:", pyogrio.list_drivers().get("GPKG"))
GeoPandas 1.1.4 | pyogrio 0.13.0 | GDAL 3.12.4
GPKG driver capabilities: raw
If .gpkg files fail to open later, the issue is often with the I/O stack rather than your Python code.
Read a layer, and what happens if you do not name one
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
first = gpd.read_file("data/city_data.gpkg")
print("no layer= :", len(first), "rows of", list(first.columns)[:2], "| warning:",
" ".join(str(caught[0].message).split())[:95] if caught else None)
parcels = gpd.read_file("data/city_data.gpkg", layer="parcels")
print("layer='parcels':", len(parcels), "rows")
no layer= : 7292 rows of ['osm_id', 'code'] | warning: More than one layer found in 'city_data.gpkg': 'roads' (default), 'parcels', 'buildings', 'road
layer='parcels': 21253 rows
Without layer=, GeoPandas read the first layer and warned that there were others. The warning is easy to miss in a script, and the first layer is only the one you wanted by luck — name the layer every time.
Inspect the GeoDataFrame after loading
print(parcels.head(3))
print(list(parcels.columns), parcels.crs)
print(parcels.geom_type.value_counts().to_dict(), len(parcels))
INSPIREID geometry
0 32383481 POLYGON ((495533.11 299678.86, 495533.15 29967...
1 32314855 POLYGON ((494274.49 299788.28, 494332.31 29970...
2 58985263 POLYGON ((494533.53 299672.66, 494529.72 29966...
['INSPIREID', 'geometry'] EPSG:27700
{'Polygon': 21253} 21253
The first rows confirm attributes loaded as expected; the columns catch naming issues; the CRS confirms the spatial reference; the geometry types and the count confirm the layer is the one you meant.
Write a GeoDataFrame to a new GeoPackage file
To write a GeoDataFrame to GeoPackage, use to_file() with the GPKG driver.
roads = gpd.read_file("data/city_data.gpkg", layer="roads")
primary = roads[roads["fclass"] == "primary"]
primary.to_file("output/primary_roads.gpkg", layer="primary_roads", driver="GPKG")
print(len(primary), "primary roads ->", gpd.list_layers("output/primary_roads.gpkg").to_dict("records"))
56 primary roads -> [{'name': 'primary_roads', 'geometry_type': 'LineString'}]
This is the standard pattern after filtering, cleaning, or analysis.
Write multiple layers to the same GeoPackage
A GeoPackage can store several layers in one file: write each GeoDataFrame to the same path with a different layer=.
buildings = gpd.read_file("data/city_data.gpkg", layer="buildings")
# centroids in a projected CRS: British National Grid for Rutland
building_centroids = buildings.to_crs(27700).copy()
building_centroids["geometry"] = building_centroids.geometry.centroid
output_file = "output/processed_data.gpkg"
Path(output_file).unlink(missing_ok=True)
primary.to_file(output_file, layer="primary_roads", driver="GPKG")
building_centroids.to_file(output_file, layer="building_centroids", driver="GPKG")
print(gpd.list_layers(output_file).to_dict("records"))
[{'name': 'primary_roads', 'geometry_type': 'LineString'}, {'name': 'building_centroids', 'geometry_type': 'Point'}]
The second write added a layer; it did not replace the file. Each layer keeps its own CRS — the roads stayed in WGS 84 and the centroids are in British National Grid — which a GeoPackage allows and a consumer has to notice.
Replace, add or append — know which one you are doing
counts = lambda: {r["name"]: len(gpd.read_file(output_file, layer=r["name"]))
for r in gpd.list_layers(output_file).to_dict("records")}
primary.head(10).to_file(output_file, layer="primary_roads", driver="GPKG") # same layer name
print("rewrite a layer: ", counts())
primary.head(10).to_file(output_file, layer="primary_roads", driver="GPKG", mode="a") # mode="a"
print("append (mode='a'):", counts())
rewrite a layer: {'building_centroids': 5513, 'primary_roads': 10}
append (mode='a'): {'building_centroids': 5513, 'primary_roads': 20}
Writing to an existing layer name replaced that layer and left the others alone; mode="a" appended to it. Neither asked first. For automation, the safest pattern is to write to a new output file rather than modify an important source file in place.
Rutland layers read selectively and written fast
The data used on this page
Rutland's roads and buildings from Geofabrik's OpenStreetMap extract of 1 January 2026 (© OpenStreetMap contributors, ODbL), HM Land Registry's INSPIRE parcels for the county (© Crown copyright and database rights; OGL), and a table of the extract's road classes with no geometry, all in one GeoPackage, data/city_data.gpkg.
import time
from pathlib import Path
import geopandas as gpd
import pandas as pd
import requests
from osgeo import gdal
gdal.UseExceptions()
for folder in ("data", "output"):
Path(folder).mkdir(exist_ok=True)
if not Path("data/city_data.gpkg").exists():
session = requests.Session()
Path("data/rutland.shp.zip").write_bytes(session.get(
"https://download.geofabrik.de/europe/united-kingdom/england/rutland-260101-free.shp.zip", timeout=300).content)
Path("data/Rutland.zip").write_bytes(session.get(
"https://use-land-property-data.service.gov.uk/datasets/inspire/download/Rutland.zip", timeout=300).content)
osm = "/vsizip/data/rutland.shp.zip/"
gdal.VectorTranslate("data/city_data.gpkg", osm + "gis_osm_roads_free_1.shp", layerName="roads")
gdal.VectorTranslate("data/city_data.gpkg", "/vsizip/data/Rutland.zip/Land_Registry_Cadastral_Parcels.gml",
accessMode="append", layerName="parcels", SQLStatement="SELECT INSPIREID FROM PREDEFINED")
gdal.VectorTranslate("data/city_data.gpkg", osm + "gis_osm_buildings_a_free_1.shp", accessMode="append",
layerName="buildings")
gdal.VectorTranslate("data/city_data.gpkg", gdal.OpenEx("data/city_data.gpkg"), accessMode="append",
layerName="road_classes", format="GPKG",
SQLStatement="SELECT fclass, count(*) AS segments FROM roads GROUP BY fclass", SQLDialect="SQLite",
geometryType="NONE")
print(Path("data/city_data.gpkg").stat().st_size // 1_000_000, "MB")
22 MB
Example 1: Read only what you need
subset = gpd.read_file("data/city_data.gpkg", layer="parcels", columns=["INSPIREID"], rows=100)
print("columns= and rows=:", subset.shape)
named = gpd.read_file("data/city_data.gpkg", layer="roads", where="name LIKE 'Station%'")
print("where=:", len(named), "segments of", sorted(named["name"].unique()))
table = gpd.read_file("data/city_data.gpkg", layer="road_classes")
print("a table with no geometry:", type(table).__name__, table.sort_values("segments", ascending=False).head(3).to_dict("records"))
columns= and rows=: (100, 2)
where=: 23 segments of ['Station Approach', 'Station Road']
a table with no geometry: DataFrame [{'fclass': 'service', 'segments': 2597}, {'fclass': 'footway', 'segments': 1273}, {'fclass': 'residential', 'segments': 1217}]
columns=, rows= and where= are handed to GDAL, so only what you ask for is read. A layer with no geometry comes back as a plain pandas DataFrame.
Example 2: Long field names and wide tables
wide = parcels.head(50).copy()
wide["registered_title_number_long_name"] = "LT000000"
wide.to_file("output/wide.gpkg", layer="wide")
wide.to_file("output/wide.shp")
print("GeoPackage keeps:", [c for c in gpd.read_file("output/wide.gpkg").columns if c.startswith("reg")])
print("shapefile keeps: ", [c for c in gpd.read_file("output/wide.shp").columns if c.startswith("reg")])
GeoPackage keeps: ['registered_title_number_long_name']
shapefile keeps: ['registered']
GeoPackage is SQLite underneath, so field names are not truncated to ten characters, and there is no 255-field cap or 2 GB file limit as there is for a shapefile.
Example 3: Engine and write speed
for engine in ("pyogrio", "fiona"):
target = Path(f"output/parcels_{engine}.gpkg")
target.unlink(missing_ok=True)
start = time.perf_counter()
parcels.to_file(target, layer="parcels", engine=engine)
print(f"{engine:8} wrote {len(parcels):,} parcels in {time.perf_counter() - start:.2f} s")
pyogrio wrote 21,253 parcels in 0.14 s
fiona wrote 21,253 parcels in 1.22 s
GeoPandas 1.x uses pyogrio by default, and it writes in bulk; Fiona writes feature by feature. If an old script sets engine="fiona", that alone can be the slow part.
One SQLite file, many layers
A GeoPackage is a single-file spatial format commonly used in QGIS and GIS data exchange. In practical workflows, it is useful because:
- it stores spatial data in one
.gpkgfile - it can contain multiple vector layers, and plain tables
- it avoids the many sidecar files used by shapefiles
- it fits well in automated workflows and data delivery
Compared with shapefiles, GeoPackages are usually easier to manage because you move one file instead of several related files like .shp, .shx, .dbf, and .prj.
GeoPandas works well for this task because it gives you direct file access through read_file() and to_file(). Once the data is loaded, you can immediately use normal GeoPandas operations for filtering rows, cleaning attributes, reprojecting data, running spatial joins, and calculating centroids, buffers, or overlays.
Layer names, CRS checks and slow large files
Layer name is missing or incorrect
for name in ("Parcels", "parcel"):
try:
print(f"layer={name!r}:", len(gpd.read_file("data/city_data.gpkg", layer=name)), "rows")
except Exception as exc:
print(f"layer={name!r}:", type(exc).__name__ + ":", " ".join(str(exc).split())[:90])
layer='Parcels': 21253 rows
layer='parcel': DataLayerError: Layer 'parcel' could not be opened
A GeoPackage is SQLite, and its layer names matched case-insensitively — Parcels found parcels — but a misspelling failed with an error naming the layer. List the layers first and copy the name; code that relies on the case-insensitivity breaks the day the data arrives as a shapefile folder or a PostGIS schema.
Check CRS before writing output
Reading and writing files does not fix CRS problems automatically, and a GeoPackage will happily hold layers in different CRSs, as the multi-layer example showed. Check gdf.crs before export, and reproject if the consumer expects one CRS.
Centroids should be calculated in a projected CRS
Centroid calculations on a geographic CRS are misleading, and Web Mercator (EPSG:3857) is not a good substitute — it distorts area increasingly away from the equator. Use a local projected CRS, such as British National Grid (EPSG:27700) for Rutland, as the multi-layer example did.
Invalid geometries can cause later processing problems
A layer may load successfully even if some geometries are invalid. That may break overlays, buffering, or spatial joins later. Validate geometry quality early if the source data is messy.
Large GeoPackage files may load slowly
If the file contains many layers or large feature counts, read only the layer — and the columns and rows — you need, as example 1 does.
Internal links
For background, see How Vector File Formats Work in Python GIS.
A GeoPackage can also carry its own metadata: see How to store metadata inside a GeoPackage.
For related tasks, see How to Read a Shapefile in Python with GeoPandas and How to Reproject Spatial Data in Python (GeoPandas).
If GeoPackage loading fails, use How to Fix GeoPandas File Read Errors for GeoPackage and Shapefile.
FAQ
How do I read a .gpkg file in Python?
Use GeoPandas:
import geopandas as gpd
gdf = gpd.read_file("data/file.gpkg", layer="parcels")
If the file has one layer, layer= may not be necessary. If it has multiple layers, specify the layer name.
How do I see all layers inside a GeoPackage?
Use gpd.list_layers(), available since GeoPandas 1.0:
import geopandas as gpd
print(gpd.list_layers("data/file.gpkg"))
It returns a DataFrame of layer names and geometry types; a table with no geometry shows NaN for the type.
Can GeoPandas write multiple layers to one GeoPackage?
Yes: write each GeoDataFrame to the same .gpkg path with a different layer= name. The second write adds a layer; writing to an existing layer name replaces only that layer.
gdf1.to_file("output/data.gpkg", layer="roads", driver="GPKG")
gdf2.to_file("output/data.gpkg", layer="parcels", driver="GPKG")
Each layer keeps its own CRS, so check them if a consumer expects one.
Do I need to specify a layer name when reading a GeoPackage?
Not if the file has one layer. With several, GeoPandas reads the first and warns that there are others — which in a script is easy to miss — so name the layer every time.
How do I read only some columns or rows from a large GeoPackage?
gpd.read_file accepts columns= to select specific fields and rows= to limit the count, for example gpd.read_file("data.gpkg", layer="parcels", columns=["id", "zone"], rows=100). You can also pass where= with an SQL-style filter to subset by attribute at read time.
Why is my GeoPackage write so slow, and how can I speed it up?
Check the engine. GeoPandas 1.x writes with pyogrio by default, in bulk: 21,253 Rutland parcels took 0.14 s. The same write with engine="fiona", which writes feature by feature, took 1.22 s. An old script that forces Fiona, or a GeoPandas older than 1.0, is the usual cause.
Can I append features to an existing GeoPackage layer?
Yes. Pass mode="a" to to_file, for example gdf.to_file("data.gpkg", layer="parcels", driver="GPKG", mode="a"), which adds rows to the existing layer instead of replacing it. The appended data should share the same schema and CRS as the target layer.
Does GeoPackage have the field name length and column count limits that shapefiles do?
No. GeoPackage uses SQLite underneath, so field names are not truncated to 10 characters and there is no 255-field cap or 2 GB size limit. This is a common reason to prefer .gpkg over .shp when attribute tables are wide.