How to Append Layers to an Existing GeoPackage with Fiona
Open the existing container with fiona.listlayers(path) to see what is already there, then call fiona.open(path, "a", driver="GPKG", layer="new_layer", schema=..., crs=...) — the append mode adds a new named layer (or appends features to an existing one) while leaving every other layer in the file untouched.
This page belongs to the Fiona & OGR Driver Configuration guide, which covers explicit driver binding and schema control. Where the batch conversion walkthrough builds a fresh file from a directory of Shapefiles, this page assumes the .gpkg already exists and you need to grow it safely.
Why This Matters
A GeoPackage is a multi-layer container: a single .gpkg file can hold dozens of feature tables, each with its own geometry type and coordinate reference system. Field pipelines rarely produce all of those layers in one pass. A survey app might ship a base map today and append a new inspection layer next week; an ETL job might add a roads layer on Monday and a parcels layer on Tuesday. Getting append semantics right is what keeps those incremental writes from silently destroying data.
The critical trap is write mode. Opening a GeoPackage with mode "w" and a layer name does not “add” that layer — if the file already exists, "w" truncates the entire container before writing. Mode "a" is the only safe choice for an existing file. Knowing when "a" creates a brand-new layer versus when it appends rows to an existing one is the difference between a clean incremental load and a corrupted deliverable.
Prerequisites
- Python 3.9+
fiona1.9+ with GDAL/OGR 3.4+ (pip install fiona[all]orconda install -c conda-forge fiona gdal)- GDAL built with the
GPKGdriver enabled (the default for conda-forge builds) - An existing
.gpkgfile you have write access to - Familiarity with Fiona schema dicts (a
{"geometry": ..., "properties": {...}}mapping)
Primary Method
# Fiona 1.9+ / GDAL 3.4+ — append a new layer to an existing GeoPackage
import fiona
def append_layer(
gpkg_path: str,
layer_name: str,
schema: dict,
crs: str,
features: list,
) -> str:
"""
Add a new named layer to an existing GeoPackage without clobbering
the layers already stored in the file.
Raises ValueError if the layer name is already present, so an
accidental re-run cannot silently merge into the wrong table.
"""
existing = fiona.listlayers(gpkg_path)
if layer_name in existing:
raise ValueError(
f"Layer {layer_name!r} already exists in {gpkg_path}; "
f"existing layers: {existing}"
)
with fiona.open(
gpkg_path,
mode="a", # append to the container — never "w" on an existing file
driver="GPKG",
layer=layer_name, # a name not yet present creates a fresh layer
schema=schema,
crs=crs,
) as dst:
dst.writerecords(features)
return layer_name
The key detail: in mode "a", passing a layer name that does not yet exist creates that layer inside the container; passing a name that does exist appends features to it (and your schema/crs arguments are ignored in favour of the layer’s stored definition). The explicit listlayers guard above turns a silent append into a loud error, which is what you want during development.
Step-by-Step Walkthrough
1. List the layers already in the file
Always inspect before you write. fiona.listlayers reads the GeoPackage gpkg_contents metadata table and returns the current layer names.
import fiona
existing = fiona.listlayers("field.gpkg")
print(existing) # e.g. ['basemap', 'survey_points']
2. Read the schema and CRS of a reference layer
If your new layer must be consistent with an existing one, pull its definition rather than hand-writing a schema:
with fiona.open("field.gpkg", layer="survey_points") as ref:
ref_schema = dict(ref.schema)
ref_crs = ref.crs
print(ref_schema) # {'geometry': 'Point', 'properties': {...}}
print(ref_crs) # CRS.from_epsg(4326)
3. Define the schema for the new layer
Each layer carries its own schema, so a new inspections layer can have completely different columns and geometry type from survey_points:
inspection_schema = {
"geometry": "Polygon",
"properties": {
"inspection_id": "int",
"inspector": "str:64",
"passed": "bool",
"recorded_at": "datetime",
},
}
4. Open in append mode and create the layer
Because inspections is not yet in the container, mode "a" creates it:
sample = [
{
"geometry": {
"type": "Polygon",
"coordinates": [[(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]],
},
"properties": {
"inspection_id": 1,
"inspector": "A. Okafor",
"passed": True,
"recorded_at": "2026-07-11T09:30:00",
},
}
]
with fiona.open(
"field.gpkg", "a", driver="GPKG",
layer="inspections", schema=inspection_schema, crs="EPSG:4326",
) as dst:
dst.writerecords(sample)
5. Append more features to that same layer later
Re-opening the file in mode "a" with an existing layer name appends rows instead of creating a table. Note that you omit schema/crs — the layer already defines them:
more = [
{
"geometry": {
"type": "Polygon",
"coordinates": [[(2, 2), (2, 3), (3, 3), (3, 2), (2, 2)]],
},
"properties": {
"inspection_id": 2,
"inspector": "R. Devi",
"passed": False,
"recorded_at": "2026-07-11T14:05:00",
},
}
]
with fiona.open("field.gpkg", "a", driver="GPKG", layer="inspections") as dst:
dst.writerecords(more)
6. Match the CRS when appending to an existing layer
Appending features whose coordinates are in a different projection than the target layer writes silently wrong geometry — Fiona does not reproject on write. If your incoming data uses another CRS, transform the geometries first with fiona.transform.transform_geom so they match the layer’s stored coordinate reference system before calling write.
Verification
Confirm the new layer landed and no existing layer lost features:
import fiona
for layer in fiona.listlayers("field.gpkg"):
with fiona.open("field.gpkg", layer=layer) as lyr:
print(f"{layer}: {len(lyr)} features, CRS={lyr.crs.to_epsg()}")
A quick SQL cross-check against the container’s contents registry confirms the driver registered the layer with a geometry column:
-- GeoPackage: every feature layer is registered here
SELECT table_name, data_type, srs_id
FROM gpkg_contents
WHERE data_type = 'features';
Alternative Approaches or Edge Cases
Overwriting a single layer. GDAL 3.4+ accepts a layer creation option to replace just one layer while keeping the rest of the container. Open with mode="w" plus the OVERWRITE="YES" layer option so only the named layer is dropped and recreated:
with fiona.open(
"field.gpkg", "w", driver="GPKG", layer="inspections",
schema=inspection_schema, crs="EPSG:4326",
layer_options={"OVERWRITE": "YES"},
) as dst:
dst.writerecords(sample)
Without OVERWRITE="YES", plain "w" on an existing file path truncates the whole GeoPackage — use this option deliberately.
Schema drift on append. If your incoming records carry a property absent from the target layer’s schema, that property is dropped silently. Compare set(feature["properties"]) against the layer schema keys before a bulk append and fail loudly on mismatch.
Troubleshooting
fiona.errors.DriverError: Failed to create GeoPackage ... : sqlite3_open failed
Cause: The parent directory does not exist, or the file is open (and locked) by another process such as QGIS. Fix: Ensure the directory exists and close any GIS application holding the file. For concurrent writers, see how to retry locked database writes in SQLite.
ValueError: Record's geometry type does not match collection schema
Cause: You are appending a MultiPolygon to a layer declared as Polygon (or vice versa). Fix: Create the layer with the Multi* variant so it accepts both single and multipart geometries, matching the promotion strategy used in the batch conversion walkthrough.
New layer silently missing after the run
Cause: The file was opened with mode "w" instead of "a", truncating the container, or the process crashed before the with block closed and flushed the transaction. Fix: Use "a" for existing files and always let the with block exit normally so the layer commit is flushed to disk.
Related
- Fiona & OGR Driver Configuration — parent guide: driver binding, schema enforcement, and CRS control
- How to Batch Convert Shapefiles to GeoPackage with Fiona — build a fresh multi-layer GeoPackage from a directory of Shapefiles
- Managing Spatial Reference Systems in SQLite — keep per-layer CRS definitions consistent when appending
- How to Retry Locked Database Writes in SQLite — handle contention when appends collide with other writers