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="newlayer",…

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+
  • fiona 1.9+ with GDAL/OGR 3.4+ (pip install fiona[all] or conda install -c conda-forge fiona gdal)
  • GDAL built with the GPKG driver enabled (the default for conda-forge builds)
  • An existing .gpkg file you have write access to
  • Familiarity with Fiona schema dicts (a {"geometry": ..., "properties": {...}} mapping)

Primary Method

python
# 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.

python
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:

python
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:

python
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:

python
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:

python
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:

python
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:

sql
-- 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:

python
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.