How to Set Up a Python GIS Environment That Actually Works

Problem statement

pip install geopandas succeeds. Then this happens:

>>> import geopandas
>>> gdf = geopandas.read_file("parcels.gpkg")
DriverError: unsupported driver: 'GPKG'

>>> gdf.to_crs(27700)
CRSError: Invalid projection: EPSG:27700

>>> import rasterio
ImportError: libgdal.so.32: cannot open shared object file

The package installed. The stack underneath it did not, or installed twice, or installed a version that disagrees with the one another package expects.

Python GIS is unusual: geopandas, rasterio and fiona are thin Python wrappers over large C and C++ libraries β€” GEOS, PROJ and GDAL. Getting a working environment is mostly about making sure exactly one copy of each of those exists and that every Python package is talking to it.

Quick answer

Pick one installer and use it for everything. Mixing is what breaks.

# Option A β€” pip with wheels. Simplest, works for most people.
python -m venv .venv && source .venv/bin/activate
pip install --only-binary=:all: geopandas rasterio matplotlib

# Option B β€” conda-forge. Use when you need a GDAL driver the wheels omit,
# or PostGIS/GRASS/QGIS integration.
conda create -n gis -c conda-forge --strict-channel-priority \
    python=3.12 geopandas rasterio matplotlib
conda activate gis

Then verify β€” do not assume:

# verify_env.py
import geopandas, shapely, pyproj, rasterio, fiona

print("geopandas", geopandas.__version__)
print("shapely  ", shapely.__version__, "/ GEOS", shapely.geos_version_string)
print("pyproj   ", pyproj.__version__, "/ PROJ", pyproj.proj_version_str)
print("rasterio ", rasterio.__version__, "/ GDAL", rasterio.__gdal_version__)
print("PROJ data", pyproj.datadir.get_data_dir())
print("drivers  ", sorted(d for d, m in fiona.supported_drivers.items() if "w" in m))

assert pyproj.CRS.from_epsg(27700).to_epsg() == 27700, "proj.db is missing"
assert "GPKG" in fiona.supported_drivers, "GDAL built without GeoPackage"
print("environment OK")

If that script runs clean, the environment works. If it does not, the line that fails names the problem.

Symptom Cause Fix
unsupported driver: 'GPKG' GDAL built without the driver use conda-forge, or a GDAL wheel
CRSError: Invalid projection proj.db missing or unfindable reinstall pyproj from wheels; check PROJ_DATA
libgdal.so: cannot open two GDAL copies, or none one installer only; rebuild the env
works in terminal, not in the IDE IDE using a different interpreter point the IDE at .venv/bin/python
works locally, not in CI different native versions see tests pass locally but fail in CI

The rule that prevents most of it

Two panels contrasting a single-installer environment with a mixed pip and conda environment.
Both halves work alone. Together they produce two GDALs and one confused interpreter.

Step-by-step solution

Vertical steps from choosing an installer through creating the environment, installing, verifying and pinning.
Five steps. Verifying is the one people skip and then spend an afternoon on.

1. Choose the installer before you install anything

pip with wheels β€” right for most work. Since GeoPandas 0.13 and Shapely 2.0, the wheels on PyPI bundle their own GEOS, PROJ and GDAL, so a plain pip install gets a complete stack with no system packages.

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install --only-binary=:all: geopandas rasterio

--only-binary=:all: is the important flag. It makes pip fail rather than fall back to building from source, and a source build is where the "no GDAL headers" errors come from.

conda-forge β€” right when you need something the wheels do not carry: FileGDB, Oracle or MSSQL drivers, GRASS, SAGA, or a specific GDAL version.

conda create -n gis -c conda-forge --strict-channel-priority python=3.12 geopandas rasterio

--strict-channel-priority prevents conda mixing defaults and conda-forge builds, which produces exactly the two-copies problem this whole page is about.

Never both. pip install rasterio inside a conda environment that already has GDAL from conda installs a second GDAL inside the wheel, and which one loads depends on library search order.

2. Create an isolated environment, always

python -m venv .venv

Not optional. Installing GIS packages into the system Python competes with whatever your OS package manager has installed, and on Linux that frequently includes an older GDAL that something else depends on.

Add it to .gitignore and never commit it:

.venv/

3. Install, then verify against the four checks

Run verify_env.py from the Quick answer. Four things matter:

pyproj.CRS.from_epsg(27700)              # proj.db is present and readable
fiona.supported_drivers["GPKG"]          # the drivers you need exist
shapely.geos_version                     # GEOS is recent enough for make_valid
rasterio.__gdal_version__                # rasterio and fiona agree on GDAL

The fourth deserves a note. fiona and rasterio each bundle GDAL in their wheels. If their versions disagree wildly, the one imported first wins and the other may misbehave. Installing both in one pip install command lets the resolver pick a compatible pair:

pip install --only-binary=:all: geopandas rasterio fiona    # one command, one resolution

4. Pin what you have, before you need to reproduce it

pip freeze > requirements.txt            # minimum
pip install pip-tools && pip-compile      # better: pins transitive deps too
conda env export --no-builds > environment.yml

Pinning Python packages does not pin GEOS/PROJ/GDAL, because those arrive inside the wheels. What it does pin is which wheels, and since a given wheel version always bundles the same native version, that is enough for reproducibility in practice.

For genuine byte-level reproducibility across machines, the answer is a container β€” see how to containerise a Python GIS pipeline.

5. Point your editor at the same interpreter

The most common "but it works in the terminal" cause:

which python
# /home/you/project/.venv/bin/python      ← this path goes in the IDE
python -c "import sys; print(sys.executable)"
  • VS Code: Command Palette β†’ Python: Select Interpreter β†’ the .venv path.
  • PyCharm: Settings β†’ Project β†’ Python Interpreter β†’ Add β†’ Existing environment.
  • Jupyter: install a kernel from inside the environment, or the notebook uses a different Python entirely:
pip install ipykernel
python -m ipykernel install --user --name gis --display-name "Python (gis)"

6. Add the optional pieces deliberately

pip install --only-binary=:all: \
    matplotlib mapclassify folium contextily     # plotting and basemaps
pip install --only-binary=:all: \
    pyogrio                                       # faster I/O than fiona
pip install --only-binary=:all: \
    "psycopg[binary]" sqlalchemy geoalchemy2      # PostGIS

pyogrio is worth installing on day one. GeoPandas uses it as the I/O engine when present, and it is several times faster than fiona for large files β€” see fiona vs pyogrio.

Code examples

Example 1: a project bootstrap script

#!/usr/bin/env bash
# bootstrap.sh β€” one command from clone to working environment
set -euo pipefail

PYTHON=${PYTHON:-python3.12}

$PYTHON -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip

pip install --only-binary=:all: -r requirements.txt
python tools/verify_env.py

echo "ready β€” activate with: source .venv/bin/activate"
# requirements.txt
geopandas==1.0.1
rasterio==1.3.11
pyogrio==0.10.0
matplotlib==3.9.2

set -euo pipefail means the script stops at the first failure rather than reporting success after a broken install.

Example 2: the verification script, worth keeping

# tools/verify_env.py β€” run after install, and as the first step in CI
import sys

REQUIRED_DRIVERS = ["GPKG", "GeoJSON", "ESRI Shapefile"]
MIN_GEOS = (3, 11, 0)

def main() -> int:
    problems = []
    try:
        import geopandas, shapely, pyproj, rasterio, fiona
    except ImportError as exc:
        print(f"import failed: {exc}")
        return 1

    print(f"python    {sys.version.split()[0]}")
    print(f"geopandas {geopandas.__version__}")
    print(f"shapely   {shapely.__version__} / GEOS {shapely.geos_version_string}")
    print(f"pyproj    {pyproj.__version__} / PROJ {pyproj.proj_version_str}")
    print(f"rasterio  {rasterio.__version__} / GDAL {rasterio.__gdal_version__}")
    print(f"engine    {geopandas.options.io_engine or 'auto'}")

    if shapely.geos_version < MIN_GEOS:
        problems.append(f"GEOS {shapely.geos_version} < {MIN_GEOS}")

    try:
        pyproj.CRS.from_epsg(27700)
    except Exception as exc:
        problems.append(f"proj.db unusable ({exc}); data dir = {pyproj.datadir.get_data_dir()}")

    missing = [d for d in REQUIRED_DRIVERS if d not in fiona.supported_drivers]
    if missing:
        problems.append(f"GDAL missing drivers: {missing}")

    if problems:
        print("\nPROBLEMS:")
        for p in problems:
            print(f"  βœ— {p}")
        return 1
    print("\nenvironment OK")
    return 0

if __name__ == "__main__":
    sys.exit(main())

Keeping this in the repo pays off twice: once when a new person clones it, and again as the first step in CI, where it turns a confusing mid-suite failure into a clear one at the top of the log.

Example 3: a smoke test that exercises the whole stack

# tools/smoke.py β€” does the stack actually do GIS?
import geopandas as gpd
from shapely.geometry import Point
import tempfile, pathlib

gdf = gpd.GeoDataFrame(
    {"id": [1, 2]},
    geometry=[Point(-3.19, 55.95), Point(-3.20, 55.94)],
    crs="EPSG:4326",
)

projected = gdf.to_crs(27700)                       # PROJ works
assert projected.crs.is_projected
assert 300_000 < projected.geometry.x.iloc[0] < 400_000

buffered = projected.buffer(50)                     # GEOS works
assert buffered.area.iloc[0] > 7000

with tempfile.TemporaryDirectory() as tmp:
    out = pathlib.Path(tmp) / "smoke.gpkg"
    projected.to_file(out, driver="GPKG")           # GDAL write works
    back = gpd.read_file(out)                       # GDAL read works
    assert back.crs == projected.crs
    assert len(back) == 2

print("smoke test passed β€” PROJ, GEOS and GDAL all working")

Version numbers tell you what is installed. This tells you it works.

Explanation

Stack showing Python packages over GEOS, PROJ and GDAL, annotated with what each installer provides.
Wheels bundle the bottom layers; conda installs them as separate packages. Mixing gives you both.

import geopandas loads a package that is mostly orchestration. The actual work happens in three native libraries:

  • GEOS β€” geometry operations: buffer, intersection, validity, predicates. Reached through Shapely.
  • PROJ β€” coordinate transformation, and the proj.db database of every EPSG code. Reached through pyproj.
  • GDAL/OGR β€” reading and writing every file format, plus raster handling. Reached through fiona, pyogrio and rasterio.

Those libraries are shared objects (.so, .dll, .dylib), loaded by the operating system at import time. The OS resolves each one by name through a search path β€” and it loads exactly one copy per process, whichever it finds first.

That single fact explains nearly every environment failure. If a conda-installed GDAL and a wheel-bundled GDAL are both present, the process gets one of them, and the Python package expecting the other calls into an ABI it was not compiled against. Sometimes that is a clean ImportError; sometimes it is a segfault; occasionally it appears to work and returns wrong answers.

It also explains why the wheels approach works so well now. Each wheel ships its own private copy of the native libraries with mangled symbol names, so rasterio's GDAL and fiona's GDAL can coexist without fighting. That is the entire reason pip install geopandas became reliable β€” and the reason mixing it with a system or conda GDAL undoes the guarantee.

The proj.db problem is a variation on the same theme. PROJ needs a data directory, found via a compiled-in path or the PROJ_DATA environment variable. Wheels bundle the file and set the path correctly; a container that installs PROJ from apt and pyproj from pip can end up with the variable pointing at the apt location while the wheel expects its own β€” hence EPSG:27700 resolving in one environment and not another.

Edge cases or notes

  • Apple Silicon: use native arm64 wheels or conda-forge osx-arm64. Rosetta-emulated x86 packages work but are slow, and mixing architectures fails at import.
  • Windows: the old advice about Gohlke's unofficial wheels is obsolete β€” PyPI wheels work. Do not mix them with OSGeo4W.
  • Docker: ghcr.io/osgeo/gdal images already contain GDAL; install with --no-binary there so pip does not add a second copy.
  • PROJ_DATA vs PROJ_LIB: PROJ 9 renamed it. Some tooling still sets the old name; set both if you are debugging.
  • GDAL_DATA matters for some drivers and is set automatically by wheels. If you set it manually, you probably have a mixing problem.
  • Jupyter runs its own kernel. !pip install in a notebook may install into a different environment than the kernel uses.
  • geopandas.options.io_engine lets you force pyogrio or fiona when the two disagree about a file.
  • Fiona 1.10 requires GDAL β‰₯ 3.4. Old distro GDAL packages are a common cause of a failed source build.

FAQ

pip or conda?

pip with --only-binary=:all: for most work β€” the wheels are complete and it is simpler. conda-forge when you need a GDAL driver, a GIS application binding, or a specific GDAL version the wheels do not offer.

Can I mix pip and conda in one environment?

Avoid it for anything that carries native libraries. If you must, install everything native from conda first and use pip only for pure-Python packages.

Why does EPSG:27700 fail when EPSG:4326 works?

4326 is often handled without a database lookup; other codes need proj.db. Print pyproj.datadir.get_data_dir() β€” it will point somewhere empty or wrong.

Do I still need GDAL installed system-wide?

No. Modern wheels bundle everything. A system GDAL is only needed for command-line tools like ogr2ogr, and even then it can live outside the Python environment.

Should I install pyogrio?

Yes. GeoPandas uses it automatically when present and it is substantially faster than fiona for large files.

How do I know which GDAL is being used?

rasterio.__gdal_version__ and fiona.__gdal_version__. If they differ, you have two copies, and that is worth fixing before anything else.

What Python version should I use?

One release behind the newest is the safe choice β€” GIS wheels usually lag a few months behind a new Python release.