Fixing “DLL load failed” for GDAL on Windows

This page answers one question: your GDAL extension installs on Windows but import raises ImportError: DLL load failed while importing _gdal: The specified module could not be found, so how do you make the bundled gdal.dll and its dependencies discoverable given that Windows has no RPATH and does not search the directory next to your .pyd? It sits inside the Platform-Specific ABI Quirks cluster of the Geospatial C-Extension Fundamentals & ABI Architecture reference, and gives you the delvewheel repair, the os.add_dll_directory shim, and the dumpbin diagnosis.

Windows DLL search order and the add_dll_directory fix A pyd import triggers the Windows loader, which searches the system directories and PATH but not the package folder holding the bundled gdal DLL, so the load fails. After delvewheel copies mangled DLLs into the package and the package init calls os.add_dll_directory on that folder, the loader finds the DLL and the import succeeds. import _gdal Windows loader searches system + PATH not the package folder ✗ add_dll_directory registers package folder ✓ bundled gdal.dll gdal-<hash>.dll found

Context & Root Cause

DLL load failed is Windows’ equivalent of cannot open shared object file: the loader could not find a DLL the .pyd depends on. Two things about Windows make this bite geospatial packages specifically. First, there is no RPATH — a Windows binary carries no embedded search path, so the $ORIGIN trick that makes Linux wheels relocatable has no counterpart. Second, since Python 3.8, the loader deliberately does not search PATH or the current directory for extension dependencies; it searches the system directories, the directories added via os.add_dll_directory, and nothing else. A gdal.dll sitting right next to your _gdal.pyd is invisible to the loader by default.

The consequence is that bundling the DLLs is necessary but not sufficient — the package must also tell the loader where they are at import time. delvewheel handles the bundling (copying and name-mangling the DLLs into the package) and injects a small loader shim, but understanding the shim is essential when it fails or when you vendor DLLs by hand. This is the Windows arm of the platform matrix in Platform-Specific ABI Quirks.

Solution / Fix

This targets Windows Server 2022 / Windows 11, MSVC 2022, GDAL 3.8.x, and delvewheel 1.x.

1. Diagnose which DLL is missing

:: In a Visual Studio Developer Command Prompt
dumpbin /dependents build\_gdal.cp312-win_amd64.pyd
:: look for gdal.dll, proj.dll, geos_c.dll among the listed imports

A dependency that is not a system DLL (KERNEL32.dll, VCRUNTIME140.dll) and not present in the package is the culprit.

2. Repair with delvewheel

delvewheel copies the dependency DLLs into the package, mangles their names to avoid clashing with any other package’s gdal.dll, and adds a _delvewheel_init call to the package:

delvewheel repair -w repaired --add-path C:\gdal\bin dist\*.whl

--add-path tells delvewheel where to find the build-time DLLs to copy. Inspect the result:

delvewheel show repaired\*.whl
:: lists the vendored, mangled DLLs now inside the wheel

3. Confirm (or add) the loader shim

delvewheel injects the shim automatically. If you vendor by hand, the package __init__.py must register the DLL folder before importing the extension:

# mypkg/__init__.py — must run BEFORE `from . import _gdal`
import os
from pathlib import Path

_dll_dir = Path(__file__).with_name("mypkg.libs")   # where the DLLs live
if _dll_dir.is_dir():
    os.add_dll_directory(str(_dll_dir))

from . import _gdal   # now the loader can resolve gdal-<hash>.dll

Verification

:: 1. The pyd's GDAL dependency must resolve to a bundled, mangled DLL
dumpbin /dependents repaired\_gdal*.pyd | findstr /i gdal
:: expected: gdal-<hash>.dll  (mangled name)
:: 2. Clean-machine import — a VM or runner with NO GDAL on PATH
pip install repaired\*.whl
python -c "from osgeo import gdal; print(gdal.__version__)"
:: expected: 3.8.x  (no 'DLL load failed')

A mangled DLL name in the dependents list and a version printed on a machine with no system GDAL confirm the fix. If it still fails, run python -X dev and read which DLL is reported missing — it is usually a second-order dependency (libtiff, libcurl) that was not on --add-path.

Pitfalls & Alternatives

Adding GDAL to PATH as a fix. On Python 3.8+ the loader ignores PATH for extension dependencies, so this appears to work only when a copy also happens to be in a system directory. Use os.add_dll_directory or delvewheel; do not rely on PATH.

Hard-coding LoadLibrary("gdal.dll"). delvewheel renames the DLL to gdal-<hash>.dll, so any code that loads it by its original name fails. Import through the package so the mangled name resolves via the registered directory.

Missing second-order dependencies. GDAL pulls proj.dll, geos_c.dll, libtiff.dll, libcurl.dll, and more. If --add-path covers only GDAL’s own folder, the transitive DLLs are left out and the import fails on the next missing name. Point --add-path at every directory holding a needed DLL, or use a pixi environment that gathers them in one prefix.