geodataframe-to-pmtiles¶
Write PMTiles archives from GeoPandas GeoDataFrames with a Python API.
Installation¶
Install from PyPI:
pip install geodataframe-to-pmtiles
Writing an archive also requires a native GDAL runtime with the PMTiles driver. Install GDAL separately with conda-forge, Homebrew, or your operating system’s package manager before writing archives.
After installing GDAL, use geodataframe_to_pmtiles.check() or the command
below to confirm that the GDAL installation and PMTiles driver work:
python -m geodataframe_to_pmtiles check
Quick start¶
Write one GeoDataFrame by passing it first and supplying the layer name:
from pathlib import Path
import geopandas as gpd
import geodataframe_to_pmtiles as gpm
points = gpd.read_file("points.geojson")
gpm.write(
points,
Path("points.pmtiles"),
layer="points",
)
Multiple named layers¶
To write more than one layer, pass a mapping. Its keys become the names stored in the archive:
boundaries = gpd.read_file("boundaries.geojson")
gpm.write(
{"points": points, "boundaries": boundaries},
Path("map.pmtiles"),
)
Every input GeoDataFrame needs an explicit CRS. Inputs in any resolvable CRS are reprojected to EPSG:4326 without changing the original frame. Assign a CRS first when a source file does not provide one:
points = points.set_crs("EPSG:4326")
output can be a path or a binary stream. This is useful when an application
needs archive bytes instead of a file:
from io import BytesIO
archive = BytesIO()
gpm.write({"points": points}, archive)
archive.seek(0)
gpm.write options¶
The two positional inputs are layers and output; all other inputs are
keyword-only. Use layer with the single-GeoDataFrame form, not the mapping
form.
Option |
Description |
|---|---|
|
A single |
|
A string or |
|
Required for a single GeoDataFrame and omitted for a mapping. It must be a non-empty string without null characters. |
|
Archive-wide minimum zoom, from 0 through 22; defaults to |
|
Archive-wide maximum zoom, from 0 through 22; defaults to |
|
Optional per-layer zoom overrides; see Per-layer zoom ranges below. |
|
Optional tileset name, stored in archive metadata when non-empty. Defaults to an empty string. |
|
Optional human-readable description, stored in archive metadata when non-empty. Defaults to an empty string. |
|
Optional string stored as TileJSON |
|
|
|
|
|
Optional geometry simplification factor in tile-coordinate units (4,096 per tile). The default, |
Per-layer zoom ranges¶
Use layer_zooms to assign different minimum and maximum zooms to individual
layers within one archive. Omitted keys inherit the archive-wide min_zoom or
max_zoom. This is useful when you want contour layers visible at all zooms
while restricting a dense data layer to high-zoom tiles only:
gpm.write(
{"contours": contours_gdf, "data": points_gdf},
Path("map.pmtiles"),
min_zoom=0,
max_zoom=8,
layer_zooms={
"contours": {"minzoom": 0, "maxzoom": 8},
"data": {"minzoom": 7}, # maxzoom inherits archive default (8)
},
)
The LayerZoomSpec type is a TypedDict with optional "minzoom" and
"maxzoom" integer keys:
from geodataframe_to_pmtiles import LayerZoomSpec
spec: LayerZoomSpec = {"minzoom": 7} # maxzoom omitted → archive default
layer_zooms validation runs before any GDAL object is created. The
InvalidLayerZoomError exception is raised when:
a key refers to a layer name not present in the
layersmapping,a zoom value is not an integer,
an effective zoom (after inheriting archive defaults) is outside 0–22, or
the effective minimum zoom exceeds the effective maximum zoom.
Archive behavior¶
PMTiles uses the Web Mercator latitude range (±85.05112877980659°). Features
entirely outside that range are warned about and skipped; if no features remain,
the writer raises EmptyLayerError. Features that cross the boundary continue
to GDAL for clipping.
Errors and limitations¶
The principal exceptions are MissingCRSError for a GeoDataFrame without a
CRS, UnsupportedCRSError or CRSTransformError for reprojection failures,
UnsupportedPropertyTypeError for values that cannot be written as MVT
properties, EmptyLayerError for empty layers, TileOverflowError for a
detected safe-overflow rejection, and InvalidLayerZoomError for invalid
layer_zooms entries.
MVT stores a feature in every tile it intersects, so feature counts read back from an archive can be higher than the source count. That duplication is not data loss. GDAL’s tile limits can still be reached by dense data; keep the default overflow policy unless accepting the resulting loss is intentional.
Links¶
About¶
Ben Welsh created this module in August 2026 as a spinoff of the Reuters Climate Monitor. GitHub Copilot, an AI-powered coding assistant, helped design, implement, test, and document the project.