How to Batch Export a GeoDataFrame to Multiple Formats in Python
You have finished a clean GeoDataFrame and now three different people want it three different ways: the web team wants GeoJSON, a colleague on old desktop software wants a Shapefile, and your own pipeline wants a fast GeoParquet. Writing each one by hand is tedious and easy to get subtly wrong — a missing driver, a field name that silently truncates, a CRS that does not survive the trip. This guide shows you how to export one GeoDataFrame to several formats in a single loop, how to fan a whole dictionary of layers out to a chosen format, and how to keep one failing format from taking the rest down with it.
Problem statement
You have a GeoDataFrame (or a folder of them) ready to write, and you are hitting some mix of these needs:
- One dataset, many formats — the same layer has to ship as GeoPackage, GeoJSON, and Shapefile because different consumers accept different things.
- Which driver goes with which extension — you are unsure when
to_file()guesses correctly from the file extension and when you must passdriver=explicitly. - Format trade-offs bite you late — a Shapefile truncates
population_densitytopopulati_1, a GeoJSON rounds your projected coordinates, or a format quietly drops the CRS. - GeoParquet is a different call —
to_file()does not write Parquet; you needto_parquet(), and it is easy to forget that. - One bad format aborts the batch — a single unsupported geometry or locked file raises, and you lose every other export in the run.
The goal: one repeatable pattern that maps extensions to drivers, writes every format you ask for, handles GeoParquet on its own path, and reports exactly which formats succeeded and which failed.
Quick answer
Keep a small {extension: driver} map, loop over it, and call gdf.to_file(path, driver=driver) for each vector format — to_file() can infer the driver from the extension, but passing it explicitly removes all ambiguity. Handle GeoParquet separately with gdf.to_parquet(). Wrap each write in its own try/except so one unsupported format does not abort the others, and collect a success/failure row per format.
import geopandas as gpd
from pathlib import Path
gdf = gpd.read_file("data/clean/sites.gpkg") # any GeoDataFrame you want to export
out_dir = Path("data/exports")
out_dir.mkdir(parents=True, exist_ok=True)
# extension -> OGR driver name
FORMATS = {
".gpkg": "GPKG",
".geojson": "GeoJSON",
".shp": "ESRI Shapefile",
}
results = []
for ext, driver in FORMATS.items():
out_path = out_dir / f"sites{ext}"
try:
gdf.to_file(out_path, driver=driver)
results.append((ext, "ok"))
except Exception as exc:
results.append((ext, f"error: {exc}"))
# GeoParquet uses its own writer, not to_file()
try:
gdf.to_parquet(out_dir / "sites.parquet")
results.append((".parquet", "ok"))
except Exception as exc:
results.append((".parquet", f"error: {exc}"))
for ext, status in results:
print(f"{ext:10} {status}")
That is the whole job. The rest of this article explains each choice — the driver argument, the format trade-offs, and the error handling — so you can adapt it instead of running it blind.
Step-by-step solution
Decide which formats you actually need
Do not export every format out of habit; each one costs disk space and can leak subtle data loss. Pick from the consumer backwards: GeoPackage for a durable working copy, GeoJSON for web maps and APIs, Shapefile only when a downstream tool demands it, and GeoParquet when you want fast columnar reads in a data pipeline. The comparison table below lays out the trade-offs.
# A GeoDataFrame is format-agnostic in memory; the format is chosen at write time.
formats_needed = [".gpkg", ".geojson"] # keep the list tight
Map each extension to its OGR driver
GeoPandas writes vector formats through GDAL/OGR, and every format has a driver name. Keeping an explicit {extension: driver} dictionary is the single most useful habit here: it documents your intent and lets you drive the whole export from one loop.
FORMATS = {
".gpkg": "GPKG",
".geojson": "GeoJSON",
".shp": "ESRI Shapefile",
".fgb": "FlatGeobuf",
}
to_file() will infer the driver from a recognised extension, so gdf.to_file("out.geojson") usually works with no driver=. But inference fails for ambiguous or less common extensions, and being explicit costs one keyword argument and removes an entire class of surprises.
Write one GeoDataFrame to each format
Loop over the map and write the same gdf to each path. Build the filename from a common stem plus the extension so every export is traceable back to one source.
import geopandas as gpd
from pathlib import Path
gdf = gpd.read_file("data/clean/sites.gpkg")
out_dir = Path("data/exports")
out_dir.mkdir(parents=True, exist_ok=True)
stem = "sites"
for ext, driver in FORMATS.items():
gdf.to_file(out_dir / f"{stem}{ext}", driver=driver)
Handle GeoParquet on its own path
GeoParquet is not an OGR vector format in the same way, and to_file() is the wrong tool for it. GeoPandas provides a dedicated to_parquet() writer that stores geometry and CRS in the GeoParquet metadata. Treat it as a special case in your export code rather than trying to force it through the driver map.
gdf.to_parquet(out_dir / "sites.parquet") # GeoParquet, columnar and fast
# read it back with gpd.read_parquet(...)
Add per-format error handling
Different formats reject different data — Shapefile refuses mixed geometry types, GeoJSON insists on being written in EPSG:4326 by the spec, a locked file blocks a rewrite. Wrap each write so a single failure is logged and the loop keeps going, exactly as you would for a folder batch.
results = []
for ext, driver in FORMATS.items():
try:
gdf.to_file(out_dir / f"{stem}{ext}", driver=driver)
results.append((ext, "ok"))
except Exception as exc:
results.append((ext, f"error: {exc}"))
Report what was written
Collect one row per format and print a small summary, or write it to CSV. This is your record of which deliverables actually exist on disk — do not skip it, because a silently missing export is the kind of thing nobody notices until the day someone needs it.
for ext, status in results:
print(f"{ext:10} {status}")
Code examples
Example 1: Export one GeoDataFrame to a list of formats
The core pattern — one gdf, an {ext: driver} map, one loop.
import geopandas as gpd
from pathlib import Path
gdf = gpd.read_file("data/clean/sites.gpkg")
out_dir = Path("data/exports")
out_dir.mkdir(parents=True, exist_ok=True)
FORMATS = {
".gpkg": "GPKG",
".geojson": "GeoJSON",
".shp": "ESRI Shapefile",
".fgb": "FlatGeobuf",
}
for ext, driver in FORMATS.items():
out_path = out_dir / f"sites{ext}"
gdf.to_file(out_path, driver=driver)
print(f"wrote {out_path.name}")
Because the driver is passed explicitly, this works even for extensions that to_file() would not recognise on its own, such as .fgb for FlatGeobuf.
Example 2: Write GeoParquet with to_parquet()
GeoParquet is worth a dedicated example because the call is different. It is compact, fast to read, and preserves the CRS in metadata — ideal for pipeline hand-offs and cloud storage.
import geopandas as gpd
from pathlib import Path
gdf = gpd.read_file("data/clean/sites.gpkg")
out_dir = Path("data/exports")
out_dir.mkdir(parents=True, exist_ok=True)
# Basic write
gdf.to_parquet(out_dir / "sites.parquet")
# Optional compression for smaller files
gdf.to_parquet(out_dir / "sites_zstd.parquet", compression="zstd")
# Round-trip check
back = gpd.read_parquet(out_dir / "sites.parquet")
print(back.crs, len(back), "rows")
Note that GeoParquet keeps whatever CRS the GeoDataFrame is in — unlike GeoJSON, it does not force EPSG:4326 — so projected coordinates survive intact.
Example 3: Export a dict of named layers into one GeoPackage
A GeoPackage can hold many layers in a single file. When you have several related GeoDataFrames, write each one as a named layer= into the same .gpkg instead of scattering files.
import geopandas as gpd
from pathlib import Path
# name -> GeoDataFrame
layers = {
"sites": gpd.read_file("data/clean/sites.gpkg"),
"roads": gpd.read_file("data/clean/roads.gpkg"),
"parks": gpd.read_file("data/clean/parks.gpkg"),
}
out_path = Path("data/exports/project.gpkg")
out_path.parent.mkdir(parents=True, exist_ok=True)
for name, layer_gdf in layers.items():
layer_gdf.to_file(out_path, layer=name, driver="GPKG")
print(f"wrote layer {name}")
Each layer keeps its own schema and CRS inside the one file, which keeps a project tidy instead of sprawling across dozens of shapefiles. For the reverse direction — reading and writing GeoPackage layers — see the GeoPackage guide.
Example 4: A robust loop that collects successes and failures
This is the version to reach for on real deliverables. It combines the vector driver map and the GeoParquet special case, catches errors per format, and returns a tidy report.
import geopandas as gpd
from pathlib import Path
def export_all_formats(gdf, out_dir, stem, formats):
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
report = []
for ext, driver in formats.items():
out_path = out_dir / f"{stem}{ext}"
try:
if ext == ".parquet":
gdf.to_parquet(out_path) # special case, not to_file()
else:
gdf.to_file(out_path, driver=driver)
report.append((ext, "ok", out_path.name))
except Exception as exc:
report.append((ext, "failed", str(exc)))
return report
FORMATS = {
".gpkg": "GPKG",
".geojson": "GeoJSON",
".shp": "ESRI Shapefile",
".parquet": None, # driver unused; handled by to_parquet()
}
gdf = gpd.read_file("data/clean/sites.gpkg")
report = export_all_formats(gdf, "data/exports", "sites", FORMATS)
for ext, status, detail in report:
print(f"{ext:10} {status:7} {detail}")
Because each format is wrapped independently, a Shapefile that chokes on mixed geometry still lets the GeoPackage, GeoJSON, and GeoParquet write successfully.
Example 5: Compare formats and pick a driver from a file path
When you accept an output path from a user or config file, you need to resolve the driver from the extension. A small helper plus a lookup table does it, and doubles as documentation of the trade-offs.
| Format | Driver | Strengths | Limits |
|---|---|---|---|
| GeoPackage | GPKG |
Single file, long field names, many layers, stores CRS, no real size cap | Slightly less universal on very old tools |
| GeoJSON | GeoJSON |
Human-readable, ideal for web/APIs, wide support | Spec is EPSG:4326 only, large and lossy for precise projected data |
| Shapefile | ESRI Shapefile |
Universally accepted by legacy desktop GIS | 10-char field names, 2 GB cap, .prj sidecar, one geometry type |
| FlatGeobuf | FlatGeobuf |
Fast streaming reads, single file, spatial index | Newer, less universally supported |
| GeoParquet | (use to_parquet) |
Compact, columnar, fast, keeps any CRS | Not editable in most desktop GIS yet |
from pathlib import Path
DRIVER_BY_EXT = {
".gpkg": "GPKG",
".geojson": "GeoJSON",
".json": "GeoJSON",
".shp": "ESRI Shapefile",
".fgb": "FlatGeobuf",
}
def driver_for(path):
ext = Path(path).suffix.lower()
if ext == ".parquet":
return "PARQUET" # signal: caller should use to_parquet()
if ext not in DRIVER_BY_EXT:
raise ValueError(f"No driver mapped for extension {ext!r}")
return DRIVER_BY_EXT[ext]
def save_any(gdf, path):
driver = driver_for(path)
if driver == "PARQUET":
gdf.to_parquet(path)
else:
gdf.to_file(path, driver=driver)
save_any(gdf, "data/exports/sites.geojson")
save_any(gdf, "data/exports/sites.parquet")
Explanation
How to_file() chooses a driver
gdf.to_file(path) asks GDAL/OGR to pick a driver from the file extension. For common extensions — .gpkg, .geojson, .shp — the guess is reliable, so you can often omit driver= entirely. The inference breaks down for extensions that map to more than one driver, for uncommon formats like FlatGeobuf, or when you want to be certain in code that will be read by someone else. Passing driver="GPKG" explicitly is never wrong and always clearer, which is why every example above does it. Think of the extension as a hint and driver= as the instruction.
Why GeoParquet is a separate call
to_file() routes through OGR's vector drivers. GeoParquet, while OGR can read and write it, is idiomatically handled in GeoPandas through to_parquet() / read_parquet(), which use the Parquet ecosystem directly and write the standard GeoParquet metadata. Keeping it as an explicit branch (if ext == ".parquet") in your export code is clearer than relying on driver inference and gives you access to Parquet-specific options like compression=.
Format trade-offs in one sentence each
- GeoPackage is the sensible default working format: one file, real field names, multiple layers, CRS stored internally.
- GeoJSON is for sharing and the web, but the spec pins it to EPSG:4326 and it bloats for dense or high-precision data.
- Shapefile exists for compatibility only; its 10-character field names and single-geometry-type rule cause the most silent data loss.
- GeoParquet is the fast, compact choice for pipelines and storage, at the cost of not being editable in most desktop GIS yet.
Edge cases or notes
Shapefile truncates long field names
Shapefiles cap column names at 10 characters, so population_density becomes something like populati_1 on write, and two long names can collide. If Shapefile is one of your export targets, rename columns to short, unique names before writing, or accept that the Shapefile copy has a degraded schema while your GeoPackage keeps the full one.
GeoJSON wants EPSG:4326
The GeoJSON specification defines coordinates as WGS84 longitude/latitude. If your GeoDataFrame is in a projected CRS, reproject it with to_crs("EPSG:4326") before writing GeoJSON, otherwise you produce a technically out-of-spec file that some consumers will reject or misplace. See exporting GeoJSON for the details.
One geometry type per Shapefile and GeoJSON layer
Shapefile stores a single geometry type per file, and a GeoDataFrame mixing points and polygons will fail or split awkwardly on write. GeoPackage and GeoParquet are more forgiving. If a batch export fails only for Shapefile, a mixed-geometry column is the usual cause — filter by geom_type and write separate files per type.
Overwriting an existing GeoPackage layer
Writing the same layer= name to a GeoPackage again appends or errors depending on the mode. To replace a layer cleanly, delete the file first or pass a fresh output path. When looping over a dict of layers into one file, make sure the layer names are unique so you do not clobber an earlier write.
The CRS must be set before you export
If gdf.crs is None, GeoJSON and GeoParquet will write coordinates with no reference frame, and consumers cannot place them. Confirm the CRS is set (and correct) before the export loop; if it is missing, repair it first — see standardising CRS across a folder for the label-versus-reproject distinction.
Exporting a rendered map is a different job
Writing a GeoDataFrame to a data format is not the same as saving a picture of it. If you actually want a PNG or SVG of a plotted map, that is a Matplotlib export, not a to_file() call — see how to save a map as an image.
Internal links
- How to Batch Process GIS Files in Python
- How to Export GeoJSON in Python with GeoPandas
- How to Read and Write GeoPackage Files in Python
- How to Batch Convert Shapefiles to GeoPackage
- How to Batch Process Multiple Shapefiles in Python
- How to Save a Map as an Image with Matplotlib
FAQ
How do I export a GeoDataFrame to multiple formats at once in Python?
Keep a dictionary that maps each file extension to its OGR driver, then loop over it calling gdf.to_file(path, driver=driver) for every vector format. Handle GeoParquet separately with gdf.to_parquet(path). Example 1 and Example 4 above show the full pattern, including per-format error handling so one failure does not stop the rest.
Do I need to pass driver= to to_file()?
Not always. to_file() infers the driver from common extensions like .gpkg, .geojson, and .shp, so it often works without it. But inference fails for less common or ambiguous extensions, and being explicit makes the code self-documenting. Passing driver= is never wrong, so prefer it in any code that matters.
How do I write a GeoParquet file from a GeoDataFrame?
Use gdf.to_parquet("out.parquet"), not to_file(). GeoParquet is written through GeoPandas' Parquet writer, which stores geometry and CRS in the file's metadata and accepts options like compression="zstd". Read it back with gpd.read_parquet(...). It keeps whatever CRS the data is in, unlike GeoJSON.
Why does my Shapefile export change the column names?
Shapefiles limit field names to 10 characters, so longer names are truncated on write and can collide with each other. This is a format limitation, not a bug. Rename columns to short unique names before exporting to Shapefile, or use GeoPackage or GeoParquet, which support full-length field names.
Can I put several layers into one GeoPackage file?
Yes. Call gdf.to_file("project.gpkg", layer="name", driver="GPKG") once per layer, using a distinct layer= name each time. Every layer keeps its own schema and CRS inside the single file. Example 3 shows a dictionary of named GeoDataFrames written into one GeoPackage.
How do I stop one failing format from aborting the whole export?
Wrap each write in its own try/except block, log the exception, and continue to the next format. Example 4 collects a success or failure row per format, so a Shapefile that rejects mixed geometry still lets GeoPackage, GeoJSON, and GeoParquet write successfully. Never let a single format failure lose the others.
Which format should I export to?
Choose from the consumer backwards. Use GeoPackage as a durable working format, GeoJSON for web maps and APIs (reprojected to EPSG:4326), Shapefile only when a legacy tool requires it, and GeoParquet for fast pipeline and storage hand-offs. The comparison table in Example 5 summarises the strengths and limits of each.
Why does GeoJSON make my file bigger and less precise?
GeoJSON is plain text and, by spec, uses EPSG:4326 longitude/latitude, so dense geometries and high-precision coordinates produce large files and can lose projected precision on the round trip. For compact, lossless storage prefer GeoPackage or GeoParquet, and reserve GeoJSON for interchange where human-readability and web support matter most.