2025, Dec 18 09:00

How to fix pyproj build errors with Cython 3 in Docker (Python 3.11 Alpine): pin Cython<3.0 and upgrade pyproj

Troubleshooting Docker pip install failures on Python 3.11 Alpine: pyproj breaks with Cython 3. Fix by pinning Cython<3.0.0 and using pyproj>=3.7.1 reliably.

When a previously stable Docker build suddenly starts failing on pip install, the instinct is to blame the base image or some obscure quirk in the build chain. In this case, the failure appeared in Python 3.11 on an Alpine base and the logs pointed at pyproj being compiled with Cython, ending in the error “Unsafe C derivative of temporary Python reference used in conditional expression”. Nothing in the project changed, older versions were tried, and yet the break persisted. The root cause turned out to be a version compatibility snag between Cython 3.0+ and older pyproj releases.

Reproducing the failure

The issue showed up while building inside a python:3.11-alpine image with system dependencies for geospatial tooling and PDAL tooling prepared ahead of time. A setup like this repeatedly failed during dependency installation of a custom package that pulls pyproj (and other geospatial libs) transitively.

FROM python:3.11-alpine

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PYTHONOPTIMIZE 2

ENV PIP_DISABLE_PIP_VERSION_CHECK 1
ENV PIP_EXTRA_INDEX_URL https://internal_gitlab_pypi_registry
ENV PIP_PROGRESS_BAR off
ENV PIP_RETRIES 1
ENV PIP_ROOT_USER_ACTION ignore

WORKDIR /workspace/app

RUN apk add --no-cache gcc musl-dev linux-headers python3-dev libstdc++ g++ geos-dev proj-dev proj-util
RUN apk add --no-cache gdal pdal-dev pdal

RUN pdal --version

RUN apk add --no-cache git cmake make
RUN git clone https://github.com/PDAL/wrench.git
RUN mkdir ./wrench/build
WORKDIR ./wrench/build
RUN cmake ..
RUN make
ENV PATH="/workspace/app/wrench/build:${PATH}"

WORKDIR /opt
RUN git clone --branch 1.4.0 --single-branch https://github.com/hobuinc/untwine.git
RUN mkdir /opt/untwine/build
WORKDIR /opt/untwine/build
RUN cmake ..
RUN make
ENV PATH="/opt/untwine/build/bin:${PATH}"

WORKDIR /workspace/app
RUN pip install -r reqs/base-gpf.txt

The pip output made clear what actually failed. The relevant part is the compile-time error from Cython while building pyproj:

Unsafe C derivative of temporary Python reference used in conditional expression

What’s really going on

The failure is triggered by Cython’s stricter type checking in version 3.0+ when it compiles older pyproj sources. That stricter behavior surfaces in the exact error cited above and causes the build step “Getting requirements to build wheel” to abort for pyproj. It isn’t a pip bug, and reverting your own package versions won’t help if pyproj still resolves to an older build that clashes with Cython 3.0+.

Fix that works reliably

The practical fix is to ensure a compatible pairing: keep Cython below 3.0.0 or use a newer pyproj that’s compatible. Applying both constraints up front avoids any ambiguity during resolution.

RUN pip install "Cython<3.0.0" "pyproj>=3.7.1"

Place that line before installing your requirements so the environment already has the right toolchain and dependency version when pyproj is pulled in. A Dockerfile with the corrected flow might look like this:

FROM python:3.11-alpine

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PYTHONOPTIMIZE 2

ENV PIP_DISABLE_PIP_VERSION_CHECK 1
ENV PIP_EXTRA_INDEX_URL https://internal_gitlab_pypi_registry
ENV PIP_PROGRESS_BAR off
ENV PIP_RETRIES 1
ENV PIP_ROOT_USER_ACTION ignore

WORKDIR /workspace/app

RUN apk add --no-cache gcc musl-dev linux-headers python3-dev libstdc++ g++ geos-dev proj-dev proj-util
RUN apk add --no-cache gdal pdal-dev pdal

RUN pdal --version

RUN apk add --no-cache git cmake make
RUN git clone https://github.com/PDAL/wrench.git
RUN mkdir ./wrench/build
WORKDIR ./wrench/build
RUN cmake ..
RUN make
ENV PATH="/workspace/app/wrench/build:${PATH}"

WORKDIR /opt
RUN git clone --branch 1.4.0 --single-branch https://github.com/hobuinc/untwine.git
RUN mkdir /opt/untwine/build
WORKDIR /opt/untwine/build
RUN cmake ..
RUN make
ENV PATH="/opt/untwine/build/bin:${PATH}"

WORKDIR /workspace/app
RUN pip install "Cython<3.0.0" "pyproj>=3.7.1"
RUN pip install -r reqs/base-gpf.txt

With this change, pyproj will either use a compatible prebuilt wheel or compile cleanly under the pinned Cython. In practice, upgrading pyproj to 3.7.1 is sufficient.

Why you should care

Build toolchains evolve independently of your application code. A transitive dependency compiled with Cython can start failing even if you didn’t touch your requirements, simply because a new Cython version landed in the build environment. Recognizing the true error signal, in this case the Cython diagnostic about an unsafe C derivative, helps you fix the actual cause instead of chasing unrelated suspects.

Takeaways

If a pip build in Docker begins to fail on pyproj with the error “Unsafe C derivative of temporary Python reference used in conditional expression,” address the compatibility gap between Cython and pyproj. Preinstall Cython<3.0.0 and require pyproj>=3.7.1 before the rest of your requirements to stabilize the build. Keep an eye on transitive dependencies that compile extensions, and surface the first meaningful error line from pip logs early when you triage.