Resolving mod_spatialite Load Errors Across Platforms

When conn.loadextension() raises cannot open shared object file on Linux, image not found on macOS, or The specified module could not be found on Windows,…

When conn.load_extension() raises cannot open shared object file on Linux, image not found on macOS, or The specified module could not be found on Windows, the fix is almost always a mismatch between the library name you passed, the directories the dynamic linker searches, and the dependency libraries that sit next to mod_spatialite.

This page is a companion to the Extension Compatibility in Spatial SQLite guide. Where that guide covers the full loading workflow, this one is a focused troubleshooting reference for the single most common wall developers hit: the extension file exists, but the OS loader refuses to bring it into the process.

Why This Matters

load_extension delegates to the operating system’s dynamic linker — dlopen on Linux and macOS, LoadLibraryEx on Windows. That linker has to do three things in sequence: find the file you named, then find every shared library that file itself depends on (GEOS, PROJ, libsqlite, and a dozen others), then resolve every symbol. A failure at any of those three steps produces a terse error that names the first file it could not open, which is frequently a transitive dependency rather than mod_spatialite itself. That is why the error message often mentions libproj.so or libgeos_c even though you asked for mod_spatialite.

The stakes are highest on the exact platforms hardest to debug: a Windows field laptop with no build tools, an Android tablet with a restricted linker namespace, or a Conda environment layered on top of a system SQLite. Getting the naming, search paths, and dependency chain right once — and encoding that knowledge in your startup code — prevents the class of failure where an app that runs perfectly on your development machine dies on first launch in the field.

Three stages where extension loading failsThe dynamic linker first locates the named library file, then resolves its dependency libraries such as GEOS and PROJ, then binds symbols. Each stage maps to a distinct error message and a distinct fix: file naming, search path, and ABI matching.1. Locate filename + suffix2. Find depsGEOS · PROJ · libs3. Bind symbolsABI matchcannot openshared objectdependency notfound on pathundefinedsymbol
The same load call fails at three different stages, each with its own message and remedy.

Prerequisites

  • Python 3.9+ whose sqlite3 module permits enable_load_extension (confirm this first — an AttributeError there is a different problem entirely)
  • Command-line access to ldd (Linux), otool -L (macOS), or Dependencies/dumpbin (Windows) for inspecting dependency chains
  • Knowledge of where your package manager installed the binary — dpkg -L libsqlite3-mod-spatialite, brew --prefix libspatialite, or the OSGeo4W bin directory
  • Once loading succeeds, confirm the build is new enough with How to Check mod_spatialite Version Compatibility

Primary Method

A robust loader tries the platform-correct library names in order and, on failure, adds the extension’s own directory to the dependency search path before retrying. This single function resolves the majority of load errors across all three operating systems.

python
# SpatiaLite -- cross-platform extension loader
import os
import sys
import sqlite3
from pathlib import Path

# Names to try, most-specific first. A bare name lets the OS loader search
# standard paths; a full path bypasses the search entirely.
CANDIDATE_NAMES = {
    "linux":   ["mod_spatialite.so", "mod_spatialite"],
    "darwin":  ["mod_spatialite.dylib", "mod_spatialite.so", "mod_spatialite"],
    "win32":   ["mod_spatialite.dll", "mod_spatialite"],
}


def load_spatialite(conn: sqlite3.Connection, extra_dir: str | None = None) -> str:
    """
    Load mod_spatialite, trying platform-appropriate names.
    If extra_dir is given, prepend it to the OS dependency search path so the
    extension can find its own GEOS/PROJ siblings. Returns the name that worked.
    """
    conn.enable_load_extension(True)

    if extra_dir:
        extra_dir = str(Path(extra_dir).resolve())
        if sys.platform == "win32":
            # Windows resolves dependency DLLs from directories added here.
            os.add_dll_directory(extra_dir)
        else:
            var = "DYLD_LIBRARY_PATH" if sys.platform == "darwin" else "LD_LIBRARY_PATH"
            os.environ[var] = os.pathsep.join(
                filter(None, [extra_dir, os.environ.get(var, "")])
            )

    errors = []
    for name in CANDIDATE_NAMES.get(sys.platform, ["mod_spatialite"]):
        target = str(Path(extra_dir) / name) if extra_dir else name
        try:
            conn.load_extension(target)
            return target
        except sqlite3.OperationalError as exc:
            errors.append(f"{target}: {exc}")

    raise RuntimeError(
        "Could not load mod_spatialite. Attempts:\n  " + "\n  ".join(errors)
    )

One caveat matters for Linux and macOS: setting LD_LIBRARY_PATH/DYLD_LIBRARY_PATH inside a running Python process is only reliably honored for libraries loaded after the change. For production, prefer exporting the variable in the launch environment (systemd unit, shell wrapper, or Docker ENV). The in-process assignment above is a best-effort fallback for interactive use.

Step-by-Step Walkthrough

1. Confirm dynamic loading is even permitted

An error on enable_load_extension itself is not a load-path problem — it means your Python was built without extension support. That is a distinct failure covered in the parent guide; the rest of this page assumes enable_load_extension(True) returns cleanly.

2. Match the library name to the operating system

The file suffix differs by platform, and passing the wrong one guarantees “cannot open”:

python
# SpatiaLite -- expected filename per platform
import sys
suffix = {"linux": ".so", "darwin": ".dylib", "win32": ".dll"}[sys.platform]
print(f"mod_spatialite{suffix}")

Passing a bare "mod_spatialite" (no suffix) is often the most portable choice: SpatiaLite’s loader appends the correct suffix for the platform, provided the file is on the search path.

3. Inspect the dependency chain

When the named file exists but still will not load, the culprit is usually a missing dependency. Inspect what the binary needs:

bash
# Linux -- list dependency libraries and flag any "not found"
ldd /usr/lib/x86_64-linux-gnu/mod_spatialite.so | grep -i "not found"
bash
# macOS -- list linked libraries; look for @rpath entries that don't resolve
otool -L $(brew --prefix libspatialite)/lib/mod_spatialite.dylib

Any line reporting not found names the exact library your search path is missing — commonly libgeos_c, libproj, or librttopo.

4. Put dependencies on the search path

Once you know which sibling libraries are missing, make their directory discoverable. Set the variable in the launch environment so it applies before the process starts:

bash
# Linux -- export before launching Python
export LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
python app.py

On Windows, dependency DLLs must sit in a directory registered via os.add_dll_directory() (the primary method does this) or alongside the .dll; PATH alone is no longer honored for extension dependencies on modern Python builds.

5. Do not mix Conda and system libraries

The single most common macOS/Linux failure is a Conda-installed mod_spatialite trying to load a system libsqlite3, or vice versa. Keep the whole chain inside one environment:

bash
# Verify SpatiaLite and SQLite come from the same Conda prefix
conda list | grep -E "libspatialite|sqlite|geos|proj"

If libspatialite is from conda-forge but your Python links a system SQLite, install a matching SQLite into the environment so the ABIs agree.

Verification

Confirm the load succeeds and the extension is live by reading a version string — a load that “succeeds” but leaves functions unavailable would fail here:

python
# SpatiaLite -- prove the extension loaded and is callable
import sqlite3

conn = sqlite3.connect(":memory:")
loaded_as = load_spatialite(conn, extra_dir="/opt/spatial/lib")  # or None
print(f"Loaded via: {loaded_as}")

version = conn.execute("SELECT spatialite_version()").fetchone()[0]
print(f"spatialite_version(): {version}")
conn.close()

From the shell, the same check without Python isolates whether the problem is SQLite-level or Python-level:

bash
# SpatiaLite -- CLI load test (Linux/macOS)
sqlite3 :memory: "SELECT load_extension('mod_spatialite'); SELECT spatialite_version();"

If the CLI loads it but Python does not, the difference is the search path or DLL directory visible to the Python process, not the binary itself.

Alternative Approaches or Edge Cases

Bundle a self-contained loader. For offline field deployments, copy mod_spatialite and every dependency it reports under ldd/otool into one application-private directory and point extra_dir at it. This removes any reliance on system package managers that may not exist on the device.

Android and restricted linker namespaces. On Android, the app’s linker namespace only searches the APK’s lib/<abi>/ directory. Ship mod_spatialite and its dependencies there and load by absolute path from the extracted native library directory. Temp-file and storage constraints on that platform are covered in the security section of the parent architecture area.

pysqlite3-binary as a reset. When ABI mismatches between the interpreter’s SQLite and the extension’s SQLite are intractable, pip install pysqlite3-binary provides a self-consistent SQLite that you alias with import pysqlite3 as sqlite3, sidestepping the system SQLite entirely.

Troubleshooting

OperationalError: mod_spatialite.so: cannot open shared object file: No such file or directory

Cause: Either the named file is not on the loader’s search path, or a dependency of it is missing — the message names whichever file failed first.

Fix: Run ldd on the binary to see which library reports not found, then add that directory to LD_LIBRARY_PATH before launching. If the file itself is absent, install libsqlite3-mod-spatialite (Debian/Ubuntu) or the equivalent for your distribution.

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

Cause: A dependency DLL (typically geos_c.dll, proj.dll, or libsqlite3-0.dll) is not in a registered DLL directory. Windows reports the top-level module as missing even when the gap is a dependency.

Fix: Place all OSGeo4W or bundled DLLs in one folder and register it with os.add_dll_directory(r"C:\OSGeo4W\bin") before calling load_extension. Use the Dependencies tool to confirm every transitive DLL resolves.

OperationalError: undefined symbol: sqlite3_...

Cause: The extension was built against a different SQLite ABI than the one Python links — a symbol it expects is absent. This is a mismatch, not a missing file.

Fix: Align the versions: install a mod_spatialite built for your SQLite, or switch to pysqlite3-binary. Confirm the versions afterward with How to Check mod_spatialite Version Compatibility.