Fiona & OGR Driver Configuration for SpatiaLite & GeoPackage
Without explicit OGR driver binding, spatial pipelines silently degrade — GeoPackage files are opened with the wrong parser, geometry columns are dropped, coordinate reference systems are ignored, and production deployments fail only after reaching the field. Understanding how Fiona exposes the GDAL/OGR driver stack is the foundational engineering concern for any team building offline-first field applications, spatial ETL pipelines, or mobile mapping backends that target SQLite-based spatial containers.
This guide is part of the Python Integration & Database Workflows section. It covers the complete arc from driver validation through schema-enforced writes, batch transaction control, and CI-level environment pinning — with runnable code and diagnostic patterns tested against GDAL/OGR 3.4+.
Prerequisites
Before configuring drivers, satisfy these baseline requirements. Missing any one of them is the leading cause of silent driver fallbacks and ImportError in production.
Validate driver availability before writing a single line of pipeline code:
# Validate required OGR drivers for GeoPackage and SpatiaLite pipelines
import fiona
from fiona.env import Env
def assert_spatial_drivers() -> None:
"""Raise RuntimeError if required OGR drivers are missing from the GDAL build."""
required = {"GPKG", "SQLite"}
with Env():
available = set(fiona.list_drivers().keys())
missing = required - available
if missing:
raise RuntimeError(
f"Missing OGR drivers: {missing}. "
"Reinstall via conda-forge or check GDAL_DATA path."
)
assert_spatial_drivers()
Both GPKG and SQLite must be present. If either is absent, your GDAL build lacks the corresponding format plugin — reinstall from conda-forge or use a pre-built binary wheel that includes these plugins.
Concept & Specification Reference
Fiona does not implement spatial I/O itself; it is a Python interface over the GDAL/OGR C library. Understanding this stack prevents misdiagnosed errors.
Driver Registration Model
| Component | Role | Key Constraint |
|---|---|---|
fiona.env.Env() | Initialises the GDAL/OGR runtime, registers format drivers exactly once per process | Must wrap all I/O; omitting it causes random driver registration order in multi-threaded code |
fiona.open(..., driver="GPKG") | Selects the GPKG format plugin explicitly | Omitting driver= triggers extension-based auto-detection which fails for .sqlite files containing GeoPackage tables |
fiona.crs.CRS | Wraps PROJ CRS definitions used during write | CRS.from_epsg() is safer than raw WKT strings; requires PROJ_LIB to resolve datum grids |
| Creation options (keyword args) | Driver-level parameters for indexing, column naming, versioning | Passed as keyword arguments directly to fiona.open(), not as a dict; unrecognised keys are silently ignored |
GeoPackage vs SpatiaLite Driver Differences
GeoPackage and SpatiaLite share the same SQLite engine but diverge at the driver level. The table below maps the key differences that affect driver configuration. For a detailed look at how SpatiaLite metadata tables differ from GeoPackage’s gpkg_contents and gpkg_geometry_columns registry tables, see the linked reference.
| Aspect | GeoPackage (GPKG driver) | SpatiaLite (SQLite driver with SPATIALITE=YES) |
|---|---|---|
| Metadata tables | gpkg_contents, gpkg_geometry_columns, gpkg_spatial_ref_sys | geometry_columns, spatial_ref_sys, spatialite_history |
| Geometry column default name | geom | geometry |
| Spatial index type | R-tree via gpkg_rtree_index | R-tree via SpatiaLite virtual tables |
| Driver creation option | SPATIAL_INDEX=YES, VERSION=1.2 | SPATIALITE=YES, SPATIAL_INDEX=YES |
| OGC compliance | GeoPackage 1.2 / 1.3 | None mandated |
The GEOMETRY_NAME creation option controls the geometry column name written into these metadata tables. A mismatch between the name declared in schema and the name registered in the metadata table causes silent geometry column drops on read.
Schema Contract
Fiona enforces schema as a Python dict with two keys:
schema = {
"geometry": "Point", # OGR geometry type string
"properties": {
"feature_id": "int", # fiona.schema.FIELD_TYPES_MAP key
"name": "str",
"captured_at": "date",
}
}
Use fiona.schema.FIELD_TYPES_MAP to enumerate valid property types. Passing unsupported types raises FionaValueError at dataset open time, not at write time — catching it early avoids partial writes.
Step-by-Step Implementation
1. Initialise a Controlled Environment Context
Wrap all I/O inside fiona.env.Env() to isolate driver registration and prevent global state pollution in multi-threaded pipelines. The environment manager ensures driver plugins load exactly once per process and are torn down cleanly when the block exits.
import fiona
from fiona.env import Env
from fiona.errors import DriverError
def safe_open_spatial(path: str, driver: str, mode: str = "r"):
"""Open a spatial file with explicit driver binding and isolated environment."""
try:
with Env():
return fiona.open(path, mode=mode, driver=driver)
except DriverError as e:
raise RuntimeError(f"Failed to initialise {driver} for {path}: {e}") from e
This pattern is critical in serverless or containerised deployments where cold starts may reset GDAL_DATA and PROJ_LIB environment variables. Configure Connection Pooling & Lifecycle Management at a higher level to avoid reinitialising the GDAL environment on every request.
2. Declare the Target Driver Explicitly
Never rely on extension-based auto-detection when targeting GeoPackage or SpatiaLite. Auto-detection parses the file suffix and matches against registered drivers; it fails when:
- Files use
.sqliteextensions but contain GeoPackage metadata tables - Multiple drivers claim the same suffix (e.g.,
GPKGandSQLiteboth recognise.gpkgin some builds) - Non-spatial tables are present in the same file, confusing driver heuristics
# Good: explicit driver prevents fallback to incorrect parsers
with Env():
src = fiona.open("survey_sites.gpkg", mode="r", driver="GPKG")
# Risky: extension-based detection may resolve the wrong driver
with Env():
src = fiona.open("survey_sites.sqlite", mode="r") # which driver wins?
3. Define the Schema and CRS Before Opening for Write
The schema and CRS must be declared at fiona.open() time. They cannot be changed after the dataset is created; attempting to write a record with a geometry type not listed in schema["geometry"] raises FionaValueError immediately.
from fiona.crs import CRS
schema = {
"geometry": "Point",
"properties": {
"site_id": "int",
"surveyor": "str",
"elevation_m": "float",
"captured_date": "date",
}
}
crs = CRS.from_epsg(4326) # WGS 84 geographic CRS
For operations that require coordinate reference system transformations before writing, apply pyproj.Transformer to each geometry prior to calling dst.write(). Fiona does not auto-transform geometries during write.
4. Write to GeoPackage with Creation Options
Pass creation options as keyword arguments directly to fiona.open(). They are evaluated during dataset initialisation and affect indexing, column naming, and format versioning.
import fiona
from fiona.crs import CRS
from fiona.env import Env
def write_to_geopackage(
output_path: str,
records: list[dict],
schema: dict,
crs_epsg: int = 4326,
) -> int:
"""Write records to a GeoPackage layer with explicit driver and schema enforcement.
Returns the number of features written.
"""
written = 0
with Env():
with fiona.open(
output_path,
mode="w",
driver="GPKG",
schema=schema,
crs=CRS.from_epsg(crs_epsg),
SPATIAL_INDEX="YES", # Build R-tree index during layer creation
VERSION="1.2", # Target GeoPackage 1.2 for maximum compatibility
GEOMETRY_NAME="geom", # Match the name registered in gpkg_geometry_columns
) as dst:
for record in records:
if record.get("geometry") is None:
continue # Fiona raises FionaValueError on None geometry
dst.write(record)
written += 1
return written
For SpatiaLite targets, replace driver="GPKG" with driver="SQLite" and add SPATIALITE="YES" as a creation option. The SPATIALITE="YES" flag instructs the driver to emit SpatiaLite metadata tables rather than the default OGR/SQLite schema.
# SpatiaLite write — same pattern, different driver and creation options
with fiona.open(
"survey_sites.sqlite",
mode="w",
driver="SQLite",
schema=schema,
crs=CRS.from_epsg(4326),
SPATIALITE="YES",
SPATIAL_INDEX="YES",
GEOMETRY_NAME="geom",
) as dst:
for record in records:
dst.write(record)
5. Batch Writes with Transaction Scoping
SQLite commits one transaction per write call by default, creating severe I/O bottlenecks when inserting thousands of features. Use writerecords() with explicit batching to amortise per-feature overhead:
from collections.abc import Iterator
def batch_write_geopackage(
output_path: str,
record_iterator: Iterator[dict],
schema: dict,
crs_epsg: int = 4326,
batch_size: int = 500,
) -> int:
"""Stream records into GeoPackage with controlled transaction batching.
Keeps the file handle open across batches so OGR can group writes
into fewer SQLite transactions. Returns total features written.
"""
total = 0
with Env():
with fiona.open(
output_path,
mode="w",
driver="GPKG",
schema=schema,
crs=CRS.from_epsg(crs_epsg),
SPATIAL_INDEX="YES",
GEOMETRY_NAME="geom",
) as dst:
batch: list[dict] = []
for record in record_iterator:
if record.get("geometry") is None:
continue
batch.append(record)
if len(batch) >= batch_size:
dst.writerecords(batch)
total += len(batch)
batch.clear()
if batch:
dst.writerecords(batch)
total += len(batch)
return total
writerecords() passes the group to the OGR layer in one call, letting the driver amortise per-feature overhead and minimise disk seeks. Benchmarks on 50,000-feature Shapefile-to-GeoPackage conversions show 4-8x throughput improvement over per-record write() calls.
The resulting files integrate cleanly with GeoPandas & GeoPackage Integration — geopandas.read_file() reads layers written by Fiona without any post-processing.
Validation & Verification
After writing, verify the output with ogrinfo (ships with GDAL) and Python schema assertions before promoting the file to production.
# Confirm layer exists, geometry type, and feature count
ogrinfo -al -so output_survey_sites.gpkg
# Expected output excerpt:
# Layer name: survey_sites
# Geometry: Point
# Feature Count: 4821
# Extent: (-120.123, 34.456) - (-118.234, 36.789)
# Layer SRS WKT: GEOGCS["WGS 84", ...]
import fiona
from fiona.env import Env
def validate_geopackage_layer(
path: str,
expected_driver: str = "GPKG",
expected_geom_type: str = "Point",
min_features: int = 1,
) -> dict:
"""Assert structural correctness of a written GeoPackage layer."""
with Env():
with fiona.open(path, mode="r", driver=expected_driver) as src:
meta = src.meta
count = len(src)
assert meta["driver"] == expected_driver, \
f"Driver mismatch: expected {expected_driver}, got {meta['driver']}"
assert meta["schema"]["geometry"] == expected_geom_type, \
f"Geometry type mismatch: {meta['schema']['geometry']}"
assert count >= min_features, \
f"Feature count {count} below minimum {min_features}"
return {"driver": meta["driver"], "crs": str(meta["crs"]), "count": count}
For deeper schema inspection — verifying that gpkg_geometry_columns correctly registered the geometry column name and SRID — combine the above with a direct sqlite3 PRAGMA check as described in Native sqlite3 Spatial Extensions.
Common Failure Modes & Fixes
Missing GPKG Driver
Symptom: DriverError: GPKG driver not found or GPKG absent from fiona.list_drivers().
Cause: GDAL compiled without GeoPackage format plugin, or the binary on the system PATH differs from the one Python’s Fiona links against (common when mixing pip and conda).
Fix: Reinstall from conda-forge — the metapackage enforces binary alignment:
conda install -c conda-forge fiona gdal
Alternatively, verify GDAL_DATA points to the data directory shipped with your GDAL binary:
python -c "import fiona; print(fiona.gdal_version())"
gdalinfo --version # these two version strings must match
Geometry Column Name Mismatch
Symptom: FionaValueError: Invalid geometry column on read, or None geometries in geopandas.read_file() output despite features being present.
Cause: The geometry column was written under a different name (e.g. geometry) than the one expected by the reading driver (e.g. geom). OGR registers the column name in the metadata table at write time; if the reader queries the wrong name, geometries are silently absent.
Diagnosis:
# -- SpatiaLite or GeoPackage context
import sqlite3
conn = sqlite3.connect("output_survey_sites.gpkg")
rows = conn.execute(
"SELECT table_name, column_name FROM gpkg_geometry_columns"
).fetchall()
print(rows) # reveals the registered geometry column name
Fix: Always pass GEOMETRY_NAME="geom" explicitly to fiona.open() on write and layer= with the correct layer name on read.
CRS Not Resolved — Missing PROJ Data
Symptom: CRSError: Invalid CRS or proj_create: no database context during fiona.open() write.
Cause: PROJ_LIB is not set, or points to a directory that does not contain proj.db.
Fix:
import os
import pyproj
# Let pyproj locate its own data directory and export it for GDAL
os.environ["PROJ_LIB"] = pyproj.datadir.get_data_dir()
Set this before importing Fiona in any cold-start environment (Lambda, container, ARM device).
Spatial Index Corruption After Bulk Insert
Symptom: Spatial queries return no results despite features being present; ogrinfo reports correct feature count.
Cause: The GPKG R-tree index (rtree_<layer>_geom) is out of sync after an interrupted write or a bulk insert that bypassed index maintenance triggers.
Diagnosis and fix:
# -- GeoPackage context
import sqlite3
conn = sqlite3.connect("output_survey_sites.gpkg")
# Rebuild the R-tree index for the survey_sites layer
conn.execute(
"DELETE FROM rtree_survey_sites_geom"
)
conn.execute(
"INSERT INTO rtree_survey_sites_geom "
"SELECT id, ST_MinX(geom), ST_MaxX(geom), ST_MinY(geom), ST_MaxY(geom) "
"FROM survey_sites WHERE geom IS NOT NULL"
)
conn.commit()
For spatial data serialization patterns that write directly to SQLite and bypass OGR triggers, always rebuild the R-tree index after any bulk insert.
OGR_ERROR on Locked File (WAL Artefacts)
Symptom: OGR_ERROR: Unable to write record or database is locked on retry after a failed write.
Cause: An interrupted transaction left a .gpkg-wal write-ahead log file in an incomplete state. Subsequent opens detect the WAL and refuse to proceed.
Fix: Close all Python handles, then recover the WAL:
import sqlite3
conn = sqlite3.connect("output_survey_sites.gpkg")
conn.execute("PRAGMA wal_checkpoint(TRUNCATE);")
conn.close()
Delete orphaned -wal and -shm files only if the checkpoint command above fails completely. The Transaction Scoping & Rollback Strategies guide covers WAL recovery in depth.
Performance Notes
Creation Options vs Post-Write VACUUM
Creating the spatial index during layer creation (SPATIAL_INDEX=YES) costs a write-time overhead proportional to feature count but avoids a separate index-build pass. For large datasets (>100,000 features) written in a single session, disable the index at creation, bulk-insert, then build the index in one pass and run VACUUM:
# Phase 1 — bulk insert without index
with fiona.open(path, mode="w", driver="GPKG", schema=schema,
crs=crs, SPATIAL_INDEX="NO", GEOMETRY_NAME="geom") as dst:
dst.writerecords(all_records)
# Phase 2 — build index and compact
import sqlite3
conn = sqlite3.connect(path)
conn.execute(
"SELECT CreateSpatialIndex('survey_sites', 'geom')"
)
conn.execute("VACUUM;")
conn.commit()
conn.close()
This two-phase approach reduces total write time by 30–50% on datasets above 500,000 features because the R-tree is built once over sorted data rather than maintained incrementally.
Page Cache and WAL Mode
Enable WAL mode and increase the page cache immediately after creating a new GeoPackage, before any writes:
import sqlite3
conn = sqlite3.connect("output_survey_sites.gpkg")
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA cache_size=-8000;") # 8 MB in-memory page cache
conn.execute("PRAGMA synchronous=NORMAL;")
conn.close()
# Now open with Fiona for writes
WAL mode allows concurrent readers during a write session — critical for offline-first apps that query feature counts while a background sync is writing new features. See the Connection Pooling & Lifecycle Management page for WAL mode interaction with connection pooling.
Environment Pinning & CI Validation
Pin GDAL and Fiona versions in requirements.txt or environment.yml. Never mix pip and conda for spatial packages — binary incompatibilities silently break driver registration. Add this validation fixture to your test suite:
import pytest
import fiona
from fiona.env import Env
@pytest.fixture(scope="session", autouse=True)
def validate_spatial_drivers():
"""Fail fast in CI if required OGR drivers are missing from the GDAL build."""
with Env():
available = set(fiona.list_drivers().keys())
required = {"GPKG", "SQLite", "ESRI Shapefile"}
missing = required - available
assert not missing, (
f"Missing OGR drivers: {missing}. "
"Check GDAL build or conda-forge installation."
)
For offline field deployments on constrained ARM devices or Windows containers, bundle proj-data and gdal-data alongside your application binary, and set GDAL_DATA and PROJ_LIB explicitly at process start rather than relying on system defaults.
Child Pages
- How to Batch Convert Shapefiles to GeoPackage with Fiona — step-by-step walkthrough for converting entire directories of Shapefiles to GeoPackage layers in a single automated pass
- How to Serialize MultiPolygon Geometries to WKB in Python — encode complex polygon topologies to Well-Known Binary for direct SQLite insertion and GeoPackage compliance
Related
- Python Integration & Database Workflows — parent overview: connection lifecycles, serialization, ETL orchestration, and concurrency control for SpatiaLite and GeoPackage
- GeoPandas & GeoPackage Integration — DataFrame-level spatial operations on top of files written by Fiona
- Native sqlite3 Spatial Extensions — direct
sqlite3module access for schema inspection, PRAGMA tuning, and spatial index management - Spatial Data Serialization Patterns — WKB and GeoPackage Binary encoding strategies for geometries written outside the OGR layer
- Transaction Scoping & Rollback Strategies — WAL mode, savepoints, and rollback patterns for interrupted field-sync writes