Diagnosing undefined-symbol errors in spatial extensions
This page answers one question: your extension raises ImportError: undefined symbol: GEOSGeom_createLinearRing_r (or _Py_Dealloc, or GDALCreate) at import, so how do you tell whether the fault is an under-linked dependency, a wrong-version library, or a Stable-ABI violation — and fix the right one? It sits inside the Debugging Import Errors and Linker Failures cluster of the Geospatial C-Extension Fundamentals & ABI Architecture reference, and gives you the nm/readelf decision path and the three distinct fixes.
Context & Root Cause
An undefined symbol at import time means the dynamic loader successfully opened every library the extension NEEDED, bound as many symbols as it could, and then found one symbol that no loaded object defines. Unlike cannot open shared object file, the libraries are present — the contents are wrong. Three root causes produce this identical message, and the symbol’s name tells you which.
If the symbol belongs to a geospatial library (GEOSGeom_createLinearRing_r, GDALCreate, proj_create), the extension was under-linked: the header was included and the call compiled, but the library was never added to the link line, so the reference dangles. If the symbol is a CPython internal (_Py_Dealloc, _PyObject_GC_New) while the wheel is tagged abi3, it is a Stable-ABI violation — you called a symbol outside the limited set that C-API vs CPython ABI compatibility forbids. If the symbol exists but at a version the loaded library does not provide, the wrong version of a dependency resolved at load time. The method is to read the prefix, then apply that branch’s fix.
Solution / Fix
This targets binutils (nm, readelf) on Linux, GEOS 3.11+/GDAL 3.8+, and a wheel built to the abi3 contract.
1. List the undefined symbol and read its prefix
unzip -o dist/*.whl -d /tmp/w >/dev/null
nm -D /tmp/w/*.so | grep ' U ' # 'U' = undefined, needs another object
# e.g.: U GEOSGeom_createLinearRing_r
2a. Geospatial prefix → fix the link line
The reference is real but the library is missing from the link. Add it — through CMake this is a target_link_libraries entry:
find_package(GEOS 3.11 CONFIG REQUIRED)
target_link_libraries(_geospatial_ext PRIVATE GEOS::geos_c) # was omitted
Confirm the library now appears as NEEDED:
readelf -d /tmp/w/*.so | grep NEEDED | grep -i geos
# expected: 0x... (NEEDED) Shared library: [libgeos_c.so.1]
2b. _Py prefix → replace with a limited-API call
The macro Py_LIMITED_API silences the header but does not police your call sites. Swap the internal for its public equivalent (for example, use Py_DECREF rather than reaching into _Py_Dealloc) and rebuild. The audit of which symbols are inside the limited set is in building abi3 wheels for PyProj with Py_LIMITED_API — enforce -DPy_LIMITED_API=0x03090000 so the compiler rejects the escape at build time, not the loader at import.
2c. Versioned symbol → rebuild against the matching ABI
# The symbol exists but the loaded GEOS is too old
nm -D /tmp/w/*.libs/libgeos_c.so.1 | grep createLinearRing_r || echo "missing in bundled GEOS"
If the bundled library predates the symbol, pin the build and runtime GEOS to one minor version and rebuild, the resolution described in how to fix ABI version mismatch in GDAL wheels.
Verification
# After the fix, no geospatial or _Py symbol should remain undefined
nm -D /tmp/w/*.so | grep ' U ' | grep -vE 'GLIBC|__gmon|_ITM|__cxa'
# expected: empty
# Clean-container import proves all symbols now bind
docker run --rm -v "$PWD/dist:/d" python:3.12-slim \
bash -c "pip install /d/*.whl && python -c 'from osgeo import gdal, ogr; print(\"ok\")'"
# expected: ok
An empty nm filter and a printed ok confirm every reference resolves. A remaining U line names the exact symbol still dangling — re-run the prefix classification on it.
Pitfalls & Alternatives
Adding -Wl,--no-undefined and calling it fixed. That flag turns the failure into a link-time error, which is genuinely better — but it does not resolve the symbol; you still must add the correct library. Use it to catch under-linking early, then fix the link line.
Assuming order does not matter. With static archives, a library must appear on the link line after the object that references it, or the linker discards its symbols. If a geospatial symbol is still undefined after adding the library, check link order before anything else.
Treating an _Py symbol as a link problem. You cannot “add a library” to satisfy a CPython internal in an abi3 wheel — the whole point of the Stable ABI is that those symbols are off-limits. This branch is always a source fix, never a link fix. Symbol shadowing between two vendored copies is a distinct case handled in symbol visibility and namespace isolation.
Related
- Debugging Import Errors and Linker Failures — the parent guide classifying this against missing-SONAME and version-mismatch errors.
- C-API vs CPython ABI compatibility — why a
_Pyundefined symbol is a Stable-ABI violation, and how to prevent it at compile time. - Symbol visibility and namespace isolation — when the symbol is defined twice and the wrong one binds.