Using sqlite3 with SpatiaLite Functions

Load modspatialite via Connection.enableloadextension() and Connection.loadextension(), call InitSpatialMetaData(1), then invoke geometry functions such…

Load mod_spatialite via Connection.enable_load_extension() and Connection.load_extension(), call InitSpatialMetaData(1), then invoke geometry functions such as ST_Buffer, ST_Intersects, and ST_Transform directly in SQL — no GDAL or GeoPandas required.

Why This Matters

Field GIS automation often runs on constrained hardware — ruggedized tablets, embedded ARM boards, or air-gapped survey laptops — where pulling in GDAL and its transitive dependencies is impractical. Python’s built-in sqlite3 module, combined with the lightweight Native sqlite3 Spatial Extensions provided by mod_spatialite, gives you buffering, intersection, coordinate transformation, and GeoPackage validation in a single portable .db or .gpkg file. The entire offline sync loop — collect, validate, transform, package — can run without a network connection or external spatial server.

Prerequisites

  • Python 3.9 or newer, built with SQLITE_ENABLE_LOAD_EXTENSION=1
  • SpatiaLite 5.0+ (mod_spatialite.so / .dll / .dylib on the system PATH or LD_LIBRARY_PATH)
  • proj.db accessible via the PROJ_LIB environment variable (required for ST_Transform)
  • Basic familiarity with SQLite’s PRAGMA commands and WKT geometry notation

Extension Load Flow

Before writing any code it helps to see exactly what happens when Python loads SpatiaLite. The diagram below shows the three-step initialization sequence: enabling dynamic loading, linking the shared object, and seeding the SpatiaLite metadata tables.

SpatiaLite initialization sequence in PythonThree-step flow: enable_load_extension → load_extension(mod_spatialite) → InitSpatialMetaData(1), resulting in a spatially-enabled connection ready for geometry SQL.conn =sqlite3.connect(path)Step 1 — open DBenable_load_extensionload_extension(...)Step 2 — link .so/.dllInitSpatialMetaData(1)PRAGMA journal_mode=WALStep 3 — seed metadataSpatialReadyLD_LIBRARY_PATH / PATH must include mod_spatialite location before Step 2

Primary Method

The function below handles cross-platform library resolution, safely enables extension loading, initializes spatial metadata, and returns a reusable connection object. It is the recommended entry point for any offline-first spatial pipeline.

python
# -- SpatiaLite context: sqlite3 + mod_spatialite 5.x --
import sqlite3
import platform
from typing import Optional

def init_spatialite(db_path: str, ext_name: Optional[str] = None) -> sqlite3.Connection:
    """
    Open a SQLite database, load SpatiaLite, and initialize spatial metadata.
    Returns a connection with all SpatiaLite geometry functions active.

    Args:
        db_path:  Path to the .db or .gpkg file (created if absent).
        ext_name: Override the extension name; auto-detected when None.
    """
    conn = sqlite3.connect(db_path)

    # Required: Python blocks extension loading by default for security.
    conn.enable_load_extension(True)

    if ext_name is None:
        ext_name = "mod_spatialite"
        if platform.system() == "Linux":
            # Some distros install the file as libspatialite.so instead.
            try:
                conn.load_extension(ext_name)
            except sqlite3.OperationalError:
                ext_name = "libspatialite"

    try:
        conn.load_extension(ext_name)
    except sqlite3.OperationalError as exc:
        raise RuntimeError(
            f"Cannot load SpatiaLite extension '{ext_name}'. "
            "Ensure the library is on LD_LIBRARY_PATH (Linux), PATH (Windows), "
            "or DYLD_LIBRARY_PATH (macOS)."
        ) from exc

    # Seed geometry_columns, spatial_ref_sys, and supporting metadata tables.
    # The argument '1' suppresses the EPSG SRS seed data (faster startup).
    conn.execute("SELECT InitSpatialMetaData(1)")

    # WAL mode: prevents write-lock contention during background sync on
    # offline devices. See the connection-pooling guide for multi-reader setup.
    conn.execute("PRAGMA journal_mode=WAL")
    conn.execute("PRAGMA synchronous=NORMAL")
    # Increase page cache to 16 MB for large geometry buffers in RAM.
    conn.execute("PRAGMA cache_size=-16000")

    return conn

Step-by-step Walkthrough

1. Create the feature table and register it with SpatiaLite

python
# -- SpatiaLite context: DDL for a spatial table --
def create_features_table(conn: sqlite3.Connection, srid: int = 4326) -> None:
    conn.execute("""
        CREATE TABLE IF NOT EXISTS features (
            id   INTEGER PRIMARY KEY,
            name TEXT NOT NULL
        )
    """)
    # AddGeometryColumn registers the column in geometry_columns and
    # enforces the SRID on every row — required for index creation.
    conn.execute(
        "SELECT AddGeometryColumn('features', 'geom', ?, 'GEOMETRY', 'XY')",
        (srid,)
    )
    conn.commit()

2. Insert geometries from WKT strings

python
# -- SpatiaLite context: geometry insert via GeomFromText --
def insert_feature(conn: sqlite3.Connection,
                   name: str, wkt: str, srid: int = 4326) -> None:
    conn.execute(
        "INSERT INTO features (name, geom) VALUES (?, GeomFromText(?, ?))",
        (name, wkt, srid)
    )
    conn.commit()

GeomFromText parses WKT and stores the geometry in SpatiaLite’s internal binary format, keeping it compatible with both SpatiaLite-native queries and GeoPackage binary geometry (GPB) readers downstream.

3. Create a spatial index

Always build a spatial index after bulk inserts. Without one, any call to ST_Intersects, ST_Within, or MbrWithin degrades to a full table scan.

python
# -- SpatiaLite context: R-tree index creation --
def build_spatial_index(conn: sqlite3.Connection,
                        table: str = "features",
                        col: str = "geom") -> None:
    conn.execute(f"SELECT CreateSpatialIndex('{table}', '{col}')")
    conn.commit()

The generated idx_features_geom R-tree virtual table is analogous to the R-tree spatial index structure described in the metadata tables reference.

4. Buffer and intersect

python
# -- SpatiaLite context: ST_Buffer + ST_Intersects query --
def find_within_distance(conn: sqlite3.Connection,
                         wkt_point: str,
                         distance_deg: float,
                         srid: int = 4326) -> list[tuple]:
    """
    Return all features whose geometry intersects a circular buffer
    around wkt_point. distance_deg is in decimal degrees for EPSG:4326.
    For metric projections pass a target_srid and use ST_Transform first.
    """
    return conn.execute("""
        SELECT f.id, f.name, ST_AsText(f.geom)
        FROM   features f
        WHERE  ST_Intersects(
                   f.geom,
                   ST_Buffer(GeomFromText(?, ?), ?)
               ) = 1
    """, (wkt_point, srid, distance_deg)).fetchall()

5. Transform coordinates to a projected SRS

python
# -- SpatiaLite context: coordinate transformation via ST_Transform --
def reproject_feature(conn: sqlite3.Connection,
                      feature_id: int,
                      target_srid: int) -> tuple | None:
    """
    Returns (id, wkt_in_target_srs) for the given feature.
    Requires PROJ_LIB to point at a valid proj.db.
    """
    return conn.execute("""
        SELECT id, ST_AsText(ST_Transform(geom, ?))
        FROM   features
        WHERE  id = ?
    """, (target_srid, feature_id)).fetchone()

Managing spatial reference systems in SQLite — including seeding custom EPSG codes into spatial_ref_sys — is a prerequisite for any ST_Transform call that targets a non-standard projection.

Verification

Run these checks after setup to confirm the extension loaded correctly and the spatial index is active:

python
# -- SpatiaLite context: post-init verification --
def verify_spatialite(conn: sqlite3.Connection) -> None:
    # 1. Confirm SpatiaLite version is 5.x
    (version,) = conn.execute("SELECT spatialite_version()").fetchone()
    assert version.startswith("5"), f"Expected SpatiaLite 5.x, got {version}"

    # 2. Confirm geometry_columns table exists (seeded by InitSpatialMetaData)
    (count,) = conn.execute(
        "SELECT COUNT(*) FROM sqlite_master WHERE name='geometry_columns'"
    ).fetchone()
    assert count == 1, "geometry_columns missing — InitSpatialMetaData may have failed"

    # 3. Confirm the spatial index virtual table is present
    (idx_count,) = conn.execute(
        "SELECT COUNT(*) FROM sqlite_master WHERE name='idx_features_geom'"
    ).fetchone()
    assert idx_count == 1, "Spatial index not found — run CreateSpatialIndex first"

    print(f"SpatiaLite {version} OK — metadata and index verified")

You can also verify from the shell without Python:

bash
# Confirm mod_spatialite loads and returns a version string
sqlite3 /tmp/test.db \
  ".load mod_spatialite" \
  "SELECT spatialite_version();"

Alternative Approaches

Using pysqlite3 on macOS

macOS System Integrity Protection prevents dlopen() in hardened runtimes, so the standard sqlite3 module’s enable_load_extension raises OperationalError: not authorized. The pysqlite3 package on PyPI ships its own SQLite amalgamation compiled with SQLITE_ENABLE_LOAD_EXTENSION=1 and bypasses the system restriction:

python
# macOS alternative: pip install pysqlite3
import pysqlite3 as sqlite3  # drop-in replacement
# remainder of init_spatialite unchanged

GeoPackage output path

To write a standards-compliant GeoPackage (.gpkg) rather than a raw SpatiaLite database, skip InitSpatialMetaData and use GDAL’s GPKG driver instead. See Python Integration & Database Workflows for the GDAL/OGR path, and the GeoPackage Specification Deep Dive for the mandatory gpkg_contents, gpkg_geometry_columns, and gpkg_spatial_ref_sys table contracts that differ from the SpatiaLite schema used above.

Troubleshooting

OperationalError: not authorized

Cause: On macOS with SIP or a hardened Python binary, enable_load_extension(True) is silently blocked.
Fix: Install pysqlite3 from PyPI (see above), or rebuild CPython from source with --enable-load-extension.

OperationalError: The specified module could not be found (Windows)

Cause: mod_spatialite.dll is not on the system PATH, or its MSVC runtime dependency (vcruntime140.dll) is missing.
Fix: Add the directory containing mod_spatialite.dll to PATH before the Python process starts. Install the Visual C++ Redistributable if MSVC runtime errors appear.

InitSpatialMetaData failed / PROJ errors

Cause: The PROJ_LIB environment variable does not point to a directory containing a valid proj.db, which SpatiaLite requires when it seeds spatial_ref_sys with EPSG definitions.
Fix: Set PROJ_LIB to the directory returned by python -c "import pyproj; print(pyproj.datadir.get_data_dir())", or install the proj-data OS package. Confirm with:

bash
python -c "import os; print(os.environ.get('PROJ_LIB', 'NOT SET'))"