skills/geopandas/references/data-io.md
GeoPandas 1.x defaults to pyogrio for vector-file I/O. pyogrio and Fiona are bindings to GDAL/OGR; actual formats, options, and behavior depend on the installed native GDAL and drivers.
Do not automatically pass any of these to GeoPandas/GDAL:
http://, https://, s3://, gs://, Azure, or another remote URI;/vsicurl/, /vsis3/, /vsizip/, chained virtual filesystems, or
pyogrio zip+... paths;GeoPandas officially supports URLs and GDAL supports network/archive virtual filesystems, but that capability crosses network, decompression, parser, and credential trust boundaries. Obtain explicit approval, verify source/hash and size out of band, unpack in a sandbox with resource limits, then process an allowlisted local regular file.
The bundled CLIs reject URL/VSI/archive syntax, symlinks, path traversal, and non-allowlisted suffixes.
For GDAL-backed formats:
from pathlib import Path
import pyogrio
path = Path("approved/input.gpkg")
if not path.is_file() or path.is_symlink():
raise ValueError("Expected a vetted local regular file")
layers = pyogrio.list_layers(path)
info = pyogrio.read_info(path, layer="approved_layer", force_feature_count=False)
drivers = pyogrio.list_drivers()
Record:
pyogrio.__gdal_version__, pyogrio.__gdal_geos_version__, Shapely GEOS,
pyproj PROJ, package versions, and enabled driver capabilities.list_drivers() returns capabilities containing r, w, and/or a, but a
listed driver is not proof every field/geometry is supported. Treat drivers as
an allowlist, not merely a discovered list.
read_fileStable signature:
gdf = geopandas.read_file(
filename,
bbox=None,
mask=None,
columns=None,
rows=None,
engine="pyogrio",
use_arrow=True,
)
Rules:
bbox and mask are mutually exclusive.columns=[] reads geometry without attributes; ignore_geometry=True
returns a pandas DataFrame.rows=n reads the first n rows; rows=slice(a, b) reads a slice.where= is evaluated by a driver SQL dialect. Do not concatenate untrusted
expressions.use_arrow=True requires PyArrow and speeds pyogrio bulk transfer, but does
not change parser trust or correctness requirements.limit + 1 and fail closed when the limit is exceeded.GeoPandas may use HTTP range requests or download an entire URL in memory. This skill therefore does not include remote-read examples.
pyogrio documents:
bbox/mask coordinates must use the dataset CRS;Do not treat driver FID as a portable stable feature ID.
Write a new path and reopen it:
output = Path("derived/result.gpkg")
if output.exists() or output.is_symlink():
raise FileExistsError("Choose a new output path")
gdf.to_file(
output,
layer="result",
driver="GPKG",
engine="pyogrio",
index=False,
use_arrow=True,
)
roundtrip = geopandas.read_file(output, layer="result", engine="pyogrio")
Never use implicit overwrite/append. Driver behavior varies and multi-file formats complicate atomic writes.
Traditional formats often cannot store lists, structs, arbitrary objects, or multiple geometry columns. Plan conversions and reject silent loss.
gdf.to_parquet(
"derived/result.parquet",
index=False,
compression="snappy",
geometry_encoding="WKB",
write_covering_bbox=False,
schema_version="1.0.0",
)
roundtrip = geopandas.read_parquet(
"derived/result.parquet",
columns=["feature_id", "geometry"],
)
GeoPandas 1.1.4 write semantics:
geometry_encoding="WKB" maximizes interoperability;geometry_encoding="geoarrow" requires GeoParquet 1.1.0, supports
single-geometry native encodings, and is still described as experimental;write_covering_bbox=True adds a per-row bbox column and 1.1 covering
metadata. It costs compute and may reveal precise extents;schema_version replaces the removed/deprecated old version= usage;index=None writes non-RangeIndex values as columns and stores RangeIndex as
metadata. Use an explicit stable feature-ID column instead.Read semantics:
pandas.read_parquet for a
non-spatial result;crs metadata is missing, the specification default is
OGC:CRS84;crs: null means unknown/undefined, which is different;GeoParquet 1.1 metadata requires a geo JSON value, primary_column, and
metadata for every geometry column. Geometry columns must be root-level and may
be optional; native child coordinates cannot contain nulls. edges defaults to
planar. Feature identifiers are outside the core specification, so define and
document your own stable-ID metadata/column.
Do not use bbox covering for public sensitive-location data without generalization and approval.
GeoPandas 1.0 added to_arrow() and from_arrow() using GeoArrow extension
types. These improve interchange but do not make an array self-validating:
verify extension metadata, CRS, geometry encoding/type, nulls, and active
geometry after round-trip. GeoPandas 1.1 adds to_pandas_kwargs controls for
non-geometry Arrow conversion.
Required writing dependencies are SQLAlchemy, GeoAlchemy2, and psycopg/psycopg2. Create connections from named secrets without embedding or logging a connection URL:
import os
from sqlalchemy import URL, create_engine
db_url = URL.create(
"postgresql+psycopg",
username=os.environ["GEOPANDAS_POSTGIS_USER"],
password=os.environ["GEOPANDAS_POSTGIS_PASSWORD"],
host=os.environ["GEOPANDAS_POSTGIS_HOST"],
port=int(os.environ["GEOPANDAS_POSTGIS_PORT"]),
database=os.environ["GEOPANDAS_POSTGIS_DATABASE"],
)
engine = create_engine(db_url)
Read only those named variables (or secret-manager equivalents). Never print
db_url, engine.url, exception payloads containing it, or the broader
environment.
Use parameterized values and trusted SQL identifiers:
from sqlalchemy import text
query = text(
"SELECT feature_id, geom FROM approved_schema.features "
"WHERE category = :category"
)
gdf = geopandas.read_postgis(
query,
con=engine,
geom_col="geom",
params={"category": approved_category},
chunksize=10_000,
)
read_postgis infers one CRS from the SRID of the first geometry and assigns it
to all rows unless crs= is supplied. Verify all geometries share the expected
SRID. With chunksize, it returns an iterator; validate every chunk and enforce
a total-row limit.
For writes:
with engine.begin() as connection:
gdf.to_postgis(
"derived_features",
con=connection,
schema="approved_schema",
if_exists="fail",
index=False,
chunksize=10_000,
)
if_exists="fail".replace drops an existing table and is destructive.GeoPandas 1.0 changed the default engine from Fiona to pyogrio. Differences include:
Set engine= explicitly for reproducibility and test round-trips before
migration. Do not assume identical outputs merely because both engines use GDAL.
For every output:
Use scripts/vector_inventory.py for redacted intake and
scripts/export_plan.py for a non-executing output contract.