How to Install and Configure GeoPandas on Windows

A GeoPandas install on Windows fails not because of GeoPandas itself but because its compiled C dependencies — GDAL, PROJ, Fiona, and Shapely — end up built against mismatched binaries, producing DLL load failed and PROJ data errors the moment you touch a CRS. This guide is for Windows users who want a reproducible GeoPandas environment that survives real projection work, and it sits under GeoPandas DataFrames Explained in Mastering Core Geospatial Python Libraries. If you already have an environment and are only fighting projection failures, jump straight to fixing PyProj CRS transformation errors.

One GDAL build resolves the GeoPandas stack; two conflicting GDALs break it Left: a conda-forge environment compiles GeoPandas, Fiona, pyogrio, Shapely and PyProj against a single pinned GDAL build, with PROJ_LIB wired inside the environment, so import and CRS transforms succeed. Right: mixing pip wheels and conda pulls in two incompatible GDAL builds; the Windows loader picks whichever appears first on PATH, the ABI no longer matches, import fails with a DLL load error, and a stale PROJ_LIB points PyProj at the wrong proj.db. One GDAL build resolves the whole stack — two GDALs break it conda-forge · single solver pass GeoPandas · import clean Fiona · pyogrio Shapely PyProj one pinned GDAL — every binding matches its ABI conda sets PROJ_LIB inside the env proj.db resolves → EPSG codes transform Result import geopandas — no DLL error to_crs() and read_file() both succeed reproducible across machines pip + conda mixed · two GDALs GeoPandas · import fails Fiona · pyogrio Shapely PyProj GDAL 3.8 pip wheel-bundled GDAL 3.6 conda channel loader takes whichever is first on PATH ABI no longer matches Result ImportError: DLL load failed while importing _geos / _proj / _fiona stale PROJ_LIB → wrong proj.db → CRSError
Install the whole spatial stack from conda-forge in one pass (left) so a single pinned GDAL backs every binding; mixing pip wheels and conda (right) drags in two incompatible GDAL builds, and the Windows loader's first-on-PATH choice is what surfaces the DLL load failed and CRSError failures.

Why This Approach / What Goes Wrong

GeoPandas is a thin, Pythonic layer over a stack of C libraries. It delegates file I/O to Fiona and pyogrio (both wrapping GDAL/OGR), coordinate transforms to PyProj (wrapping the PROJ library), and geometry to Shapely (wrapping GEOS). On Linux and macOS these C libraries are routinely available as system packages, so pip wheels tend to resolve against a single consistent GDAL. Windows has no system package manager for spatial binaries, so each wheel bundles its own copy of the underlying .dll files — and when two wheels bundle two incompatible GDAL builds, the loader picks whichever it finds first on PATH and the ABI no longer matches. The symptom is the notorious ImportError: DLL load failed while importing _geos (or _proj, or _fiona) that appears on import geopandas even though every package claims to be installed.

The second failure mode is the PROJ data directory. PROJ needs proj.db and the transformation grids to resolve EPSG codes into real math. Each build expects that database at a compiled-in location, and Windows users frequently have a stale PROJ_LIB environment variable left over from a QGIS or OSGeo4W install pointing at a different PROJ version's data. When PyProj loads its own PROJ but reads the wrong proj.db, you get a CRSError on a perfectly valid EPSG code — the classic "the projection exists but the transform is undefined" trap covered in depth under coordinate systems with PyProj.

Order in which PyProj searches for proj.db, and why a stale variable wins PyProj checks three candidate locations for the PROJ data directory in a fixed order. The first candidate is the PROJ_LIB environment variable, which on many Windows machines still points at an old OSGeo4W or QGIS share folder; that match ends the search immediately. The second candidate, the active conda environment's own share/proj folder, and the third, PROJ's compiled-in fallback path, are therefore never reached. The result is new PROJ binaries reading an old proj.db, which raises CRSError on a perfectly valid EPSG code; removing PROJ_LIB lets the second candidate win. PROJ data lookup — the first hit wins, and it is usually the stale one candidate locations, in order what PyProj does with it 1 · PROJ_LIB environment variable C:\OSGeo4W\share\proj — left behind by QGIS MATCH — the search stops here an older proj.db than the loaded PROJ 2 · the active environment's own data dir ...\envs\geo_win\Library\share\proj never reached this is the one conda wired up for you 3 · PROJ's compiled-in fallback path $CONDA_PREFIX/share/proj never reached Result: current PROJ binaries, an outdated proj.db → CRSError on an EPSG code that is perfectly valid Fix: unset PROJ_LIB, then step 2 wins confirm with pyproj.datadir.get_data_dir()
PyProj stops at the first candidate that resolves, so a leftover PROJ_LIB from an OSGeo4W or QGIS install silently outranks the environment's own proj.db — remove the variable and the conda path wins.

The correct approach removes both problems at the root: install the entire spatial stack from the conda-forge channel in a fresh, isolated environment. conda-forge compiles GeoPandas, GDAL, PROJ, Fiona, pyogrio, and Shapely against one pinned GDAL build and wires the PROJ data path automatically inside the environment. There is no wheel-bundled DLL collision to resolve because there is exactly one GDAL, and there is no PROJ_LIB guessing because conda sets it per-environment. pip-based installs can be made to work on Windows, but only by manually matching wheel versions — the conda-forge route is the one that is reproducible across machines.

That advice has softened since GeoPandas 1.0, and it is worth knowing why before you assume conda is mandatory. Shapely, PyProj, and pyogrio all publish Windows wheels that bundle their own GEOS, PROJ, and GDAL, and those bundled DLLs are name-mangled at build time so two copies can coexist in one process without the loader confusing them. On a machine with no conda and no OSGeo4W, pip install geopandas now genuinely works. The failure mode this guide describes has therefore narrowed rather than disappeared: it is specifically mixing installation channels — a pip wheel dropped into a conda environment, or a pip environment running on a machine where OSGeo4W has prepended its own GDAL to the system PATH. Pick one channel and stay inside it. Choose conda-forge when the same environment must also host gdal, rasterio, or a PostGIS client that should share one library build; choose pip when GeoPandas and its immediate dependencies are the whole spatial stack and you want a requirements.txt that a CI runner can reproduce without a solver.

Prerequisites

Create the environment and install the whole stack in one solver pass so conda resolves a single compatible GDAL:

conda create -n geo_win -c conda-forge python=3.12 geopandas -y
conda activate geo_win

Installing GeoPandas in the same command that creates the environment lets the solver choose Python and every C library together. Adding it later, into an environment built from the defaults channel, is the most common way to reintroduce a GDAL mismatch.

Step-by-Step Implementation

1. Create an isolated conda-forge environment.

Never install spatial packages into base. A dedicated environment keeps GDAL, PROJ, and their data directories scoped so a later project cannot silently upgrade GDAL underneath a working install.

conda create -n geo_win -c conda-forge python=3.12 geopandas -y
conda activate geo_win

2. Confirm you are on 64-bit Python.

The conda-forge spatial stack is 64-bit only; a 32-bit interpreter cannot load the DLLs at all. Check before going further.

import struct
import sys

# 64 => 64-bit interpreter (required). 32 => reinstall a 64-bit Python.
print("Pointer width:", struct.calcsize("P") * 8)
print("Executable:", sys.executable)

3. Import the stack and print the resolved versions.

A clean import with matching version floors is the fastest proof that the DLLs loaded and the builds agree.

import geopandas as gpd
import shapely
import pyproj
import fiona
import pyogrio

print("geopandas", gpd.__version__)   # >= 1.0
print("shapely  ", shapely.__version__)  # >= 2.0
print("pyproj   ", pyproj.__version__)   # >= 3.4
print("fiona    ", fiona.__version__)    # >= 1.9
print("pyogrio  ", pyogrio.__version__)  # >= 0.7

4. Confirm PROJ can find its data directory.

Before trusting any transform, ask PyProj where it is reading proj.db from. It should point inside your geo_win environment (a Library\share\proj path), not at an external OSGeo4W or QGIS install.

import pyproj

# Should resolve to a path inside the active conda env, e.g.
# ...\envs\geo_win\Library\share\proj
print("PROJ data dir:", pyproj.datadir.get_data_dir())

5. Build a GeoDataFrame with an explicit CRS.

Create geometries with Shapely and declare the CRS at construction time — never rely on an implicit or inherited projection. Coordinates for geographic data are always lon/lat order in GeoPandas.

import geopandas as gpd
from shapely.geometry import Point

# Two survey markers, declared in WGS84 (lon, lat order)
markers = gpd.GeoDataFrame(
    {"name": ["marker_north", "marker_south"]},
    geometry=[Point(-73.9857, 40.7484), Point(-118.2437, 34.0522)],
    crs="EPSG:4326",
)
print(markers.crs)  # EPSG:4326

6. Reproject to a metric CRS and read a file — the two operations that actually exercise the C stack.

A transform touches PROJ; a read_file touches GDAL/OGR through pyogrio. If both succeed, the install is genuinely healthy, not merely importable. Use estimate_utm_crs() for a correct metric zone — never Web Mercator (EPSG:3857) for measurement.

Five checks, each exercising one layer of the compiled stack Five sequential checks run left to right: interpreter architecture, importing geopandas, resolving the PROJ data path, running a CRS transform, and a file write-and-read round trip. Beneath each check sits the specific failure it catches — a 32-bit interpreter, a DLL load error from mismatched GDAL builds, a stale PROJ_LIB pointing at another proj.db, a CRSError on a valid EPSG code, and a driver error from a partial GDAL install. Only when all five pass has every layer of the C stack actually been exercised rather than merely imported. A clean import proves nothing — each layer needs its own check 1 Architecture calcsize("P") * 8 2 Import import geopandas 3 PROJ data path get_data_dir() 4 CRS transform estimate_utm_crs() 5 File round-trip to_file / read_file catches 32-bit Python — no DLL will load catches DLL load failed — two GDAL builds catches stale PROJ_LIB — another proj.db catches CRSError on a valid EPSG code catches driver error from a partial GDAL All five pass → architecture, bindings, PROJ data, transforms and drivers are genuinely healthy
Steps 5 and 6 are the two that leave the pure-Python layer: a transform proves PROJ resolved its data, and a file round-trip proves GDAL's drivers loaded — the checks a bare import geopandas cannot make.
# Reproject to the local UTM zone so any area/distance work is metric
markers_utm = markers.to_crs(markers.estimate_utm_crs())
print("Projected CRS:", markers_utm.crs)
print("Is projected:", markers_utm.crs.is_projected)

# Exercise the GDAL/OGR read+write path with a round-trip
markers.to_file("markers.gpkg", driver="GPKG")
roundtrip = gpd.read_file("markers.gpkg")
print("Read back rows:", len(roundtrip))

7. Register the environment everywhere you will actually run code.

An environment that works in the Anaconda Prompt and fails in Jupyter or an IDE is not a broken install — it is a second interpreter with a different PATH. Windows resolves DLLs relative to the running process, so a notebook started from base loads base's GDAL no matter which environment you believe you are in. Register the kernel explicitly and check from inside it.

conda activate geo_win
conda install -c conda-forge ipykernel -y
python -m ipykernel install --user --name geo_win --display-name "Python (geo_win)"
# Run this INSIDE the notebook, not in the terminal — it must report the geo_win path
import sys, pyproj
print(sys.executable)                       # ...\envs\geo_win\python.exe
print(pyproj.datadir.get_data_dir())        # ...\envs\geo_win\Library\share\proj

The same rule applies to scheduled tasks and services: launch them with the environment's absolute python.exe rather than relying on an activated shell, because a Windows service inherits the machine PATH and none of conda's activation scripts.

8. Move your working data off synced and network paths.

GeoPackage and SQLite rely on file locking that OneDrive, Dropbox, and SMB shares do not implement faithfully. The result is intermittent database is locked errors, sqlite3.OperationalError on write, and — rarely but expensively — a corrupted .gpkg. Read from a synced folder if you must, but write to a local disk path and copy the finished file back.

from pathlib import Path

# Local scratch, not C:\Users\you\OneDrive\...
work_dir = Path.home() / "geodata_local"
work_dir.mkdir(exist_ok=True)

markers.to_file(work_dir / "markers.gpkg", driver="GPKG")

Verification

Run this single script after setup. It asserts every layer of the stack — import, architecture, PROJ data path, CRS transform, and file round-trip — so a green run means the environment is production-ready.

import struct
import geopandas as gpd
import pyproj
from shapely.geometry import Point

# 1. Architecture
assert struct.calcsize("P") * 8 == 64, "Need 64-bit Python for the spatial stack"

# 2. PROJ data resolves inside this environment
data_dir = pyproj.datadir.get_data_dir()
assert "proj" in data_dir.lower(), f"PROJ data dir looks wrong: {data_dir}"

# 3. CRS declaration and transform
plots = gpd.GeoDataFrame(
    {"id": [1]}, geometry=[Point(-73.9857, 40.7484)], crs="EPSG:4326"
)
plots_utm = plots.to_crs(plots.estimate_utm_crs())
assert plots_utm.crs.is_projected, "Transform did not produce a projected CRS"

# 4. GDAL/OGR round-trip
plots.to_file("verify.gpkg", driver="GPKG")
assert len(gpd.read_file("verify.gpkg")) == 1, "File round-trip failed"

print("GeoPandas Windows environment verified — stack is ready.")
# Expected console output:
# GeoPandas Windows environment verified — stack is ready.

Edge Cases & Debugging

Frequently Asked Questions

Do I still need conda on Windows, or is pip enough now? Pip is enough if GeoPandas and its direct dependencies are the entire spatial stack, because Shapely, PyProj, and pyogrio all publish Windows wheels with their C libraries bundled and name-mangled. Conda-forge remains the better answer in two situations: when the same environment must also carry gdal, rasterio, or other libraries that should share one GDAL build rather than each bundling a private copy, and when you are on a machine that already has OSGeo4W or QGIS on PATH, since a conda environment isolates you from it more reliably. The failure this guide describes comes from mixing the two channels, not from either one on its own.

Should I just use WSL2 instead of fighting Windows? It is a legitimate answer and often the fastest one if your work is scripted rather than interactive. Inside WSL2 you get the Linux packaging story, where system GDAL and PROJ are ordinary packages and none of the DLL resolution problems exist. The costs are real though: file I/O across the Windows/Linux boundary is slow enough to matter on large datasets, so keep your data inside the Linux filesystem, and desktop GIS integration — dragging a written GeoPackage into QGIS, for instance — becomes a copy step. Use WSL2 for pipelines, a native environment for exploratory work alongside desktop tools.

Why does the same script give different results on my laptop and the build server? Because the transformation results depend on the PROJ version and the grid files present, not only on the EPSG codes in your source. A desktop with QGIS installed may have datum grids that a bare CI container does not, so the container silently falls back to a lower-accuracy operation and your coordinates move by a metre or two. Print pyproj.__proj_version__ and pyproj.datadir.get_data_dir() on both machines as the first diagnostic, then pin the versions and ship the grids — the mechanics are covered in fixing PyProj CRS transformation errors.

Is it safe to install QGIS on the same machine? Yes, provided you never let its installer's directories reach the front of your system PATH and you clear any PROJ_LIB or GDAL_DATA variables it leaves behind. QGIS ships a complete, self-consistent spatial stack that is excellent for QGIS and actively harmful to a separate Python environment, because Windows resolves DLLs and PROJ resolves its data directory by first match. Launching your Python work from the Anaconda Prompt or with an absolute interpreter path, rather than from a generic system shell, keeps the two apart in practice.

How do I make this environment reproducible for a colleague? Export an explicit environment file rather than a list of top-level names, so the solver cannot pick different builds next week: conda env export --from-history > environment.yml captures what you asked for, while conda list --explicit > spec-file.txt captures the exact builds that were installed and is what you want when a result must be reproducible byte-for-byte. Commit the explicit file alongside the code, and add the version print block from the verification script to your test suite so a drifted environment fails the build rather than the analysis.