Bundling proj.db and datum grids in a wheel
This page answers one question: your wheel bundles libproj correctly, yet at runtime PyProj raises pyproj.exceptions.DataDirError: Valid PROJ data directory not found, so how do you package proj.db and the datum-shift grids as wheel data and point PROJ at them at import — since auditwheel bundles shared objects but never data files? It sits inside the Build Artifact Structuring and Packaging cluster of the Modern Python Build Tooling & Wheel Configuration reference, and gives you the package_data layout, the runtime path shim, and the size-versus-completeness trade-off.
Context & Root Cause
PROJ splits into two things: libproj, a shared object of transformation code, and a data payload — proj.db, an SQLite database of coordinate reference systems and datum definitions, plus optional .tif grid files for high-accuracy datum shifts. The linked library is what auditwheel and delocate understand; they trace the NEEDED entries of your extension and copy the .so/.dylib into the wheel. They have no notion of data dependencies, so proj.db is silently left behind. At runtime PROJ opens the database by searching a compiled-in path and the PROJ_DATA/PROJ_LIB environment variable — neither of which points inside your wheel — and raises DataDirError.
The fix is orthogonal to library bundling: ship the data as package data and set PROJ’s search directory to the bundled location from Python before any transform runs. This is a packaging concern, which is why it lives under Build Artifact Structuring and Packaging rather than the vendoring guide — the library decision is already made; this is about the files repair tools ignore.
Solution / Fix
This targets PROJ 9.3.x and PyProj 3.6+. The layout ships proj.db inside the package and registers it at import.
1. Stage the data into the package tree
# Copy PROJ's data payload into the importable package before building
mkdir -p src/mypkg/proj_data
cp "$(pkg-config --variable=pkgdatadir proj)"/proj.db src/mypkg/proj_data/
cp "$(pkg-config --variable=pkgdatadir proj)"/proj.ini src/mypkg/proj_data/ 2>/dev/null || true
2. Declare it as package data
# pyproject.toml
[tool.setuptools.package-data]
mypkg = ["proj_data/*.db", "proj_data/*.ini", "proj_data/*.tif"]
[tool.setuptools.packages.find]
where = ["src"]
3. Point PROJ at the bundled data at import
# src/mypkg/__init__.py — runs before any pyproj transform
import os
from pathlib import Path
import pyproj
_data = Path(__file__).with_name("proj_data")
if (_data / "proj.db").exists():
# pyproj's own resolver — preferred over exporting PROJ_DATA globally
pyproj.datadir.set_data_dir(str(_data))
os.environ.setdefault("PROJ_DATA", str(_data)) # covers raw C-API callers
pyproj.datadir.set_data_dir is preferred over exporting PROJ_DATA for the whole process, because a global variable would also redirect any other PROJ-using library in the interpreter.
Verification
# 1. The data file is actually inside the wheel
unzip -l dist/*.whl | grep proj.db
# expected: mypkg/proj_data/proj.db
# 2. A transform that needs the datum DB succeeds in a clean container
docker run --rm -v "$PWD/dist:/d" python:3.12-slim bash -c "
pip install /d/*.whl &&
python -c \"import mypkg, pyproj; t=pyproj.Transformer.from_crs(4326,3857); print(t.transform(52.0,5.0))\""
# expected: a coordinate pair, not DataDirError
# 3. Confirm pyproj resolved the bundled directory, not a system one
python -c "import mypkg, pyproj; print(pyproj.datadir.get_data_dir())"
# expected: .../site-packages/mypkg/proj_data
A proj.db inside the wheel, a successful transform in a bare image, and a data-dir path pointing inside the package confirm the bundling. DataDirError in step 2 means the import shim did not run before the transform.
Pitfalls & Alternatives
Relying on PROJ_LIB from the environment. Setting PROJ_LIB in CI makes local tests pass while the shipped wheel still fails for users who never set it. Bundle the data and register it in code.
Bundling the full grid set unconditionally. The optional datum-shift .tif grids run to gigabytes. Ship proj.db (a few MB) always, but treat the high-accuracy grids as opt-in — most users are served by the database alone, and shipping them all bloats the wheel the way a vendored PROJ does in why vendoring PROJ causes wheel bloat. For grids beyond the database, enabling PROJ’s network fetch (PROJ_NETWORK=ON) is the leaner alternative.
Forgetting GDAL’s own data. GDAL has a parallel GDAL_DATA directory (gcs.csv, projection tables). A GDAL-plus-PROJ wheel must bundle and register both, following the identical pattern.
Related
- Build Artifact Structuring and Packaging — the parent guide on wheel layout,
package_data, and stagingdist/. - Why vendoring PROJ causes wheel bloat — the size trade-off that governs how much grid data to ship.
- Configuring pixi environments for wheel building — where
GDAL_DATA/PROJ_LIBcome from during the build itself.