How to Batch Convert Shapefiles to GeoPackage in Python

You have a folder of shapefiles — dozens of them, each dragging along its .dbf, .shx, .prj, and .cpg sidecars — and you are tired of the sprawl. Field names have been chopped to ten characters, one file is bumping against the 2 GB ceiling, and every time you move data around a .prj goes missing and the CRS vanishes. GeoPackage fixes all of that: one SQLite file, long field names, real data types, and either one .gpkg per source or every layer tucked into a single file. This guide shows you how to batch convert a whole folder of shapefiles to GeoPackage in Python, safely, with per-file error handling and a check that the output actually opens.

Problem statement

You have a directory of shapefiles and you want them all as GeoPackage, but the format switch is not just a rename. You are hitting some mix of these:

  • Sidecar sprawl — every shapefile is really four to six files (.shp, .dbf, .shx, .prj, .cpg), so a folder of 40 datasets is 200-plus files, and losing one sidecar corrupts the set.
  • Truncated field names — the shapefile format caps column names at 10 characters, so population_density was silently saved as populatio, and you want the full names back when it matters.
  • The 2 GB limit — the .dbf and .shp components each cap out around 2 GB, and a large layer either fails to write or is already clipped.
  • Weak type support — shapefiles collapse dates to strings, cannot store proper booleans, and mangle wide integers, whereas GeoPackage carries real SQLite types.
  • Two possible layouts — do you want one .gpkg per shapefile, or every shapefile as a named layer inside one .gpkg, and how do you write each without clobbering the last?

The goal: one repeatable script that discovers every .shp, converts it to GeoPackage in whichever layout you choose, keeps going when one file fails, and verifies the result opens.

Quick answer

Loop over the folder with pathlib, read each shapefile with gpd.read_file(), and write it out with gdf.to_file(out_path, driver="GPKG"). For one file per shapefile, build an output path from path.stem. For everything in one GeoPackage, write to a single .gpkg and pass layer=path.stem so each source becomes its own named layer. Wrap each file in its own try/except so one bad shapefile does not abort the batch.

Many shapefiles with their sidecar files collapsing into one GeoPackage with named layers.
Each shapefile and its sidecars convert to a GeoPackage — one file each, or one layer each inside a single .gpkg.
import geopandas as gpd
from pathlib import Path

src_dir = Path("data/shapefiles")
out_dir = Path("data/geopackages")
out_dir.mkdir(parents=True, exist_ok=True)

converted, failed = [], []
for path in sorted(src_dir.glob("*.shp")):
    try:
        gdf = gpd.read_file(path)
        out_path = out_dir / f"{path.stem}.gpkg"
        gdf.to_file(out_path, driver="GPKG", layer=path.stem)
        converted.append(out_path.name)
    except Exception as exc:
        failed.append((path.name, str(exc)))

print(f"Converted {len(converted)} files, {len(failed)} failed")
for name, err in failed:
    print(f"  FAIL {name}: {err}")

That is the whole job for the one-file-per-shapefile layout. The rest of this article explains each decision, shows the single-GeoPackage variant, and covers field names, error handling, and verifying the output.

Step-by-step solution

Discover the shapefiles

Use pathlib.Path plus glob to find every .shp. Globbing *.shp deliberately matches only the main component — GeoPandas pulls in the .dbf, .shx, .prj, and .cpg sidecars automatically when it reads the .shp. Use rglob instead of glob if your shapefiles are nested in subfolders.

from pathlib import Path

src_dir = Path("data/shapefiles")
shapefiles = sorted(src_dir.glob("*.shp"))

print(f"Found {len(shapefiles)} shapefiles")
for path in shapefiles:
    print(path.name)

Never glob * and expect it to work — you would try to open .dbf and .shx files directly and get errors. Match the .shp only and let the driver assemble the rest.

Choose an output layout

There are two sensible targets, and the choice shapes the rest of the script:

  • One GeoPackage per shapefileroads.shp becomes roads.gpkg. This is the closest one-to-one replacement, easy to reason about, and keeps datasets independent.
  • One GeoPackage, many layersroads.shp, sites.shp, and parks.shp all become layers inside project.gpkg. This collapses a whole folder into a single portable file, which is the strongest reason to adopt GeoPackage in the first place.

GeoPackage supports both because a .gpkg is a SQLite database that can hold any number of layers. Pick per-file when datasets are unrelated and shared separately; pick single-file when they belong to one project. If you need the format background, see how to read and write GeoPackage files in Python.

Convert one file per shapefile

Read each shapefile and write it straight back out with driver="GPKG". Build the output name from path.stem so roads.shp maps cleanly to roads.gpkg. Setting layer=path.stem names the single layer sensibly rather than leaving it as the default file stem.

import geopandas as gpd
from pathlib import Path

src_dir = Path("data/shapefiles")
out_dir = Path("data/geopackages")
out_dir.mkdir(parents=True, exist_ok=True)

for path in sorted(src_dir.glob("*.shp")):
    gdf = gpd.read_file(path)
    out_path = out_dir / f"{path.stem}.gpkg"
    gdf.to_file(out_path, driver="GPKG", layer=path.stem)
    print(f"{path.name} -> {out_path.name}")

One .shp (plus its sidecars) collapses into one self-contained .gpkg. The CRS travels inside the file, so you never have to worry about a missing .prj again.

Convert everything into one GeoPackage

To collapse the whole folder into a single file, write each shapefile to the same .gpkg with a distinct layer= name. Use path.stem as the layer name so each source file is identifiable inside the database.

import geopandas as gpd
from pathlib import Path

src_dir = Path("data/shapefiles")
out_path = Path("data/project.gpkg")
out_path.parent.mkdir(parents=True, exist_ok=True)

for path in sorted(src_dir.glob("*.shp")):
    gdf = gpd.read_file(path)
    gdf.to_file(out_path, driver="GPKG", layer=path.stem)
    print(f"Wrote layer '{path.stem}' to {out_path.name}")

Because each write targets a different layer=, they coexist in one file instead of overwriting each other. Two files with the same stem would collide — the second write replaces the first layer — so make sure your stems are unique.

Handle each file's errors independently

A real folder always has one surprise: a corrupt geometry, a shapefile missing its .dbf, or an empty file. Wrap the read-write cycle for each file in its own try/except so a single failure is logged and skipped rather than aborting an overnight batch.

import geopandas as gpd
from pathlib import Path

src_dir = Path("data/shapefiles")
out_dir = Path("data/geopackages")
out_dir.mkdir(parents=True, exist_ok=True)

failed = []
for path in sorted(src_dir.glob("*.shp")):
    try:
        gdf = gpd.read_file(path)
        gdf.to_file(out_dir / f"{path.stem}.gpkg", driver="GPKG", layer=path.stem)
        print(f"OK   {path.name}")
    except Exception as exc:
        print(f"FAIL {path.name}: {exc}")
        failed.append((path.name, str(exc)))

print(f"\nDone. {len(failed)} failed.")

Verify the output opens

Converting is not done until you have confirmed the GeoPackage reads back. List the layers in the file and read one back to check the feature count matches the source. This catches a truncated write or a permissions problem immediately, while you still remember what you ran.

import geopandas as gpd
import fiona

out_path = "data/project.gpkg"

layers = fiona.listlayers(out_path)
print(f"{out_path} contains {len(layers)} layers: {layers}")

check = gpd.read_file(out_path, layer=layers[0])
print(f"Layer '{layers[0]}': {len(check)} features, CRS {check.crs}")

If listlayers shows the layers you expect and the read-back feature count matches the source shapefile, the conversion succeeded.

Code examples

Example 1: One GeoPackage per shapefile

The simplest layout — each shapefile becomes its own .gpkg in a parallel output folder.

import geopandas as gpd
from pathlib import Path

src_dir = Path("data/shapefiles")
out_dir = Path("data/geopackages")
out_dir.mkdir(parents=True, exist_ok=True)

for path in sorted(src_dir.glob("*.shp")):
    gdf = gpd.read_file(path)
    out_path = out_dir / f"{path.stem}.gpkg"
    gdf.to_file(out_path, driver="GPKG", layer=path.stem)
    print(f"{path.name} -> {out_path.name}  ({len(gdf)} features)")

Example 2: All shapefiles into one GeoPackage with layer=path.stem

Collapse a whole folder into a single portable file, one named layer per source.

import geopandas as gpd
import fiona
from pathlib import Path

src_dir = Path("data/shapefiles")
out_path = Path("data/project.gpkg")
out_path.parent.mkdir(parents=True, exist_ok=True)

# Remove an existing file so a re-run starts clean instead of appending.
if out_path.exists():
    out_path.unlink()

for path in sorted(src_dir.glob("*.shp")):
    gdf = gpd.read_file(path)
    gdf.to_file(out_path, driver="GPKG", layer=path.stem)

print(f"Wrote {len(fiona.listlayers(out_path))} layers to {out_path.name}")

Deleting the target first avoids stale layers lingering from a previous run.

Example 3: Preserve field names that the shapefile truncated

Shapefiles cap column names at 10 characters, so the truncation already happened when the shapefile was written — GeoPackage cannot magically recover population_density from populatio. What you can do is detect the truncation and restore the intended names with a rename map before writing.

import geopandas as gpd
from pathlib import Path

# Map the 10-char shapefile names back to their intended full names.
RENAMES = {
    "populatio": "population_density",
    "median_ho": "median_household_income",
}

src_dir = Path("data/shapefiles")
out_dir = Path("data/geopackages")
out_dir.mkdir(parents=True, exist_ok=True)

for path in sorted(src_dir.glob("*.shp")):
    gdf = gpd.read_file(path)

    truncated = [c for c in gdf.columns if len(c) == 10]
    if truncated:
        print(f"{path.name}: possibly truncated -> {truncated}")

    gdf = gdf.rename(columns={k: v for k, v in RENAMES.items() if k in gdf.columns})
    gdf.to_file(out_dir / f"{path.stem}.gpkg", driver="GPKG", layer=path.stem)

Once the data is in GeoPackage, long names survive on every future write. For why the truncation happens in the first place, see why shapefile column names get truncated.

Example 4: Full batch with try/except and a summary table

The version you would actually run on a real folder — per-file error handling plus a summary you can scan or save.

import geopandas as gpd
import pandas as pd
from pathlib import Path

src_dir = Path("data/shapefiles")
out_dir = Path("data/geopackages")
out_dir.mkdir(parents=True, exist_ok=True)

rows = []
for path in sorted(src_dir.glob("*.shp")):
    try:
        gdf = gpd.read_file(path)
        out_path = out_dir / f"{path.stem}.gpkg"
        gdf.to_file(out_path, driver="GPKG", layer=path.stem)
        rows.append({"file": path.name, "features": len(gdf),
                     "fields": len(gdf.columns) - 1, "status": "ok"})
    except Exception as exc:
        rows.append({"file": path.name, "features": 0,
                     "fields": 0, "status": f"error: {exc}"})

report = pd.DataFrame(rows)
print(report.to_string(index=False))
report.to_csv(out_dir / "_conversion_report.csv", index=False)

Example 5: Read a layer back and list all layers to verify

After the batch, confirm the GeoPackage is sound. Both fiona.listlayers and pyogrio.list_layers enumerate what is inside; read one layer back to check the feature count.

import geopandas as gpd
import fiona
import pyogrio

out_path = "data/project.gpkg"

# Fiona: returns a plain list of layer names.
print("fiona:", fiona.listlayers(out_path))

# pyogrio: returns an array of [name, geometry_type] pairs.
print("pyogrio:", pyogrio.list_layers(out_path))

for name in fiona.listlayers(out_path):
    gdf = gpd.read_file(out_path, layer=name)
    print(f"  {name}: {len(gdf)} features, CRS {gdf.crs}")

If every layer reads back with the feature count you expect, the conversion is verified end to end.

Explanation

Why GeoPackage over shapefile

The shapefile format dates from the early 1990s and carries limits that GeoPackage simply does not have. A shapefile is a set of files — .shp for geometry, .dbf for attributes, .shx for the index, .prj for the CRS, .cpg for the encoding — and they must travel together; lose the .prj and the CRS is gone, lose the .dbf and the attributes are gone. Field names are capped at 10 characters, the .shp and .dbf components cap at roughly 2 GB, and attribute types are limited (no true dates, no booleans, narrow numeric precision).

GeoPackage is a single SQLite file. It holds any number of layers, stores the CRS internally per layer, supports long field names, has effectively no size limit for normal use, and carries proper SQLite data types. Converting a folder of shapefiles to GeoPackage removes the sidecar fragility and the format ceiling in one pass.

driver="GPKG" and the layer= argument

gdf.to_file(path, driver="GPKG") writes a GeoPackage. GeoPandas usually infers the driver from a .gpkg extension, but passing it explicitly makes intent obvious and avoids surprises. The layer= argument is what makes GeoPackage more than a shapefile replacement: it names the layer inside the file. Omit it and you get a single layer named after the file stem; supply a different layer= on each write to the same file and you accumulate many layers in one database. That single mechanism powers both output layouts in this guide.

Why per-file error handling matters

Reading a whole folder in one loop means one corrupt shapefile can raise an exception that kills the entire run — and if it fails on file 38 of 40, you lose the 37 that would have converted fine. Wrapping each file in try/except converts a fatal crash into a logged, skippable event. The bad file lands in your summary marked as an error; every other file still converts. For an overnight or large batch this is not optional.

Edge cases or notes

Field names were already truncated at the source

Converting to GeoPackage does not un-truncate anything. If a name was cut to populatio when the shapefile was written, that is the name GeoPandas reads. GeoPackage lets you store the full name once you rename the column (Example 3), but you must know or reconstruct the original — the information is not hiding in the shapefile.

Duplicate layer names collide in single-file mode

When you write many shapefiles into one .gpkg, two source files with the same stem produce the same layer= name, and the second write overwrites the first. If your folder has 2020/roads.shp and 2021/roads.shp, disambiguate the layer name — prefix it with the parent folder, for example layer=f"{path.parent.name}_{path.stem}".

Re-running appends rather than replacing

Writing a layer that already exists replaces that layer, but layers from a previous run that no longer have a matching source file stay behind. For a clean rebuild of a single-file GeoPackage, delete the .gpkg before the loop (Example 2) so the output reflects only the current folder.

Mixed or missing CRS across the folder

to_file writes whatever CRS each GeoDataFrame carries, so a folder of shapefiles in different CRSs produces GeoPackages in different CRSs. That is fine for a straight conversion, but if you need everything in one CRS — or some files have no .prj and read as crs=None — handle that separately. See how to standardise and repair CRS across a folder of files; this guide deliberately stays focused on the format conversion.

Empty geometries and mixed geometry types

A layer with mixed geometry types (points and polygons together) or with null geometries can write to GeoPackage but may surprise downstream tools. GeoPackage stores a declared geometry type per layer; if a shapefile mixes types, expect a generic geometry column. Inspect gdf.geom_type.unique() before writing if this matters to you.

Large folders and memory

gpd.read_file() loads an entire file into memory. Processing one file per loop iteration (as shown throughout) keeps only one GeoDataFrame resident at a time, which is what you want for a big folder. If a single shapefile is too large to fit, read it in chunks with pyogrio. To convert many independent files faster on a multi-core machine, run them in parallel — that is a separate topic covered in how to batch process multiple shapefiles in Python.

FAQ

How do I convert a whole folder of shapefiles to GeoPackage in Python?

Loop over the folder with pathlib, read each .shp with gpd.read_file(), and write it out with gdf.to_file(out_path, driver="GPKG"). Build the output name from path.stem for one file per shapefile, or write every file to the same .gpkg with a distinct layer= name to collapse the folder into a single file. Wrap each file in try/except so one bad shapefile does not stop the batch.

Should I make one GeoPackage per shapefile or put them all in one file?

Both are valid. Use one .gpkg per shapefile when the datasets are unrelated and shared separately — it stays close to the original layout. Use a single .gpkg with many named layers when the files belong to one project and you want a single portable file. GeoPackage handles both because a .gpkg is a SQLite database that can store many layers.

Does converting to GeoPackage restore my truncated field names?

No. The shapefile format truncated the names to 10 characters when it was written, so the full names are already lost by the time you read the file. GeoPackage supports long names going forward, but you must supply the intended names yourself — for example with a rename map (Example 3). See why shapefile column names get truncated for the underlying cause.

What does the layer= argument do in to_file?

layer= names the layer inside the GeoPackage. Because a .gpkg can hold many layers, writing several GeoDataFrames to the same file with different layer= names stores them side by side instead of overwriting. Writing two GeoDataFrames with the same layer= name replaces the layer, so keep your layer names unique when collapsing a folder into one file.

How do I check that the GeoPackage actually converted correctly?

List the layers with fiona.listlayers(path) or pyogrio.list_layers(path) and confirm they match what you expect. Then read one layer back with gpd.read_file(path, layer=name) and compare the feature count to the source shapefile. Matching counts and the expected layer list mean the conversion succeeded (Example 5).

Why is my second layer overwriting the first in the same GeoPackage?

You are almost certainly writing both with the same layer= name — often because two source shapefiles share a stem, so path.stem produces an identical name. Give each layer a unique name, for example by prefixing the parent folder: layer=f"{path.parent.name}_{path.stem}".

Do I need to handle the CRS during conversion?

Not for a straight format conversion — to_file writes whatever CRS each file already carries, and GeoPackage stores it internally per layer. If files are in different CRSs and you need one common CRS, or some shapefiles are missing their .prj and read as crs=None, handle that as a separate step. See how to standardise and repair CRS across a folder of files.

How do I keep one corrupt shapefile from stopping the whole batch?

Wrap the read-and-write cycle for each file in its own try/except, log the exception, append the failure to a list, and continue the loop. The bad file ends up in your summary marked as an error while every other file still converts (Example 4). Never let a single failure abort a large or overnight conversion.