Skip to content

Using Python packages on z/OS

How to install Python packages that zopen builds for z/OS, and the three ways a normal pip install goes wrong here.

Everything on this page has been checked on z/OS against Python 3.12, 3.13 and 3.14.

The short version

sh
export PIP_EXTRA_INDEX_URL="https://repo.zopen.community/pypi/wheels/simple/"
export PIP_CONSTRAINT="https://repo.zopen.community/pulp/content/constraints/zopen-constraints.txt"

python3 -m venv --system-site-packages .venv
. .venv/bin/activate

pip install cryptography

All three lines are load-bearing. Drop any one of them and the install fails — or, worse, appears to succeed and gives you something else. The rest of this page explains each, because the failures are quiet and none of them says what is actually wrong.

Why --system-site-packages

The z/OS interpreters ship with packages that PyPI would normally supply, and some of those cannot be installed from PyPI here at all. cffi is the one that matters: it ships only an sdist for this platform, and building it needs libffi headers that z/OS does not have.

ERROR: Failed building wheel for cffi

A plain python3 -m venv hides the interpreter's copy, so anything depending on cfficryptography, and most packages wrapping a C library — cannot be installed into one, whatever index or constraints you configure. Creating the environment with --system-site-packages lets it see what the interpreter already has.

Bundled versions, for reference:

package3.123.133.14
cffi1.17.12.0.02.0.0
cryptography3.3.23.3.23.3.2

Why PIP_CONSTRAINT

That second row is the problem. The interpreters bundle cryptography 3.3.2, released in February 2021, with a long list of published CVEs including certificate-validation bypasses.

pip treats it as satisfying a bare pip install cryptography and does nothing:

Requirement already satisfied: cryptography

You get a success message and stay on the vulnerable version. Nothing warns you.

The constraints file fixes this as a side effect of pinning. It says "if this package is installed, it must satisfy this specifier", and 3.3.2 fails the pin, so pip upgrades instead of skipping.

This is why constraints are better than --only-binary on z/OS rather than merely more convenient: --only-binary has nothing to say about a package that is already present.

The constraints file also caps a few third-party packages below releases that pull in dependencies which cannot be built here. It is generated — the pins come from the wheel index itself, so they cannot go stale.

Why PIP_EXTRA_INDEX_URL

PyPI has no z/OS wheels, so without the zopen index there is nothing to install but sdists, and a compiled package will not build.

https://repo.zopen.community/pypi/wheels/simple/

It is a PEP 503 simple index and can be browsed directly — that is the authoritative list of what is available, always current, which is why this page does not reproduce it.

pip resolves by version across both indexes, so a newer release on PyPI still wins over a zopen wheel. That is the other thing the constraints file prevents.

Disk space

Compiled extensions are large here, because they are linked statically — everything the library needs is inside the .so rather than resolved at run time. cryptography's extension is about 72 MB per environment, and lxml ships five extension modules.

Two consequences:

  • Budget on the order of 100 MB per virtual environment, not 10 MB.

  • Do not let pip's cache sit on a small filesystem. On many systems /u is far smaller than /tmp. A cache that fills mid-install produces errors that look like dependency problems — or, worse, a truncated .so that fails at import with CEE3512S ... reason code EF076015, which reads like a missing execute bit rather than a partial file.

    sh
    export PIP_CACHE_DIR=/tmp/pip-cache

Checking that an install actually worked

Importing the top-level package is not a real check for anything with a compiled extension — the pure-Python half imports happily while the extension is missing or unloadable. Touch the part that matters:

sh
python -c "
import cryptography
from cryptography.hazmat.primitives import hashes
hashes.Hash(hashes.SHA256()).finalize()
print(cryptography.__version__)"

If you are installing something the interpreter also bundles, assert the version too, or you may be testing the bundled copy:

sh
python -c "import cryptography; assert cryptography.__version__ == '50.0.0'"

Using uv instead of pip

uv is available as a port rather than from PyPI:

sh
zopen install uv

It reaches the wheel index and installs from it correctly, and it is substantially faster than pip. One limit decides whether it is usable for a given job:

  • Nothing that depends on cffi, which rules out cryptography, pynacl and paramiko.

It is explained below. Where it applies, use pip.

The settings map across:

pipuv
PIP_EXTRA_INDEX_URLUV_INDEX
PIP_CONSTRAINTUV_CONSTRAINT
PIP_CACHE_DIRUV_CACHE_DIR
sh
export UV_INDEX="https://repo.zopen.community/pypi/wheels/simple/"
export UV_CONSTRAINT="https://repo.zopen.community/pulp/content/constraints/zopen-constraints.txt"
export UV_CACHE_DIR=/tmp/uv-cache

uv venv .venv
uv pip install --python .venv/bin/python msgpack

UV_INDEX rather than UV_EXTRA_INDEX_URL: uv accepts the latter but reports it as deprecated in favour of --index.

uv will not use the interpreter's bundled packages

This is the important difference, and it is why cffi is fatal here.

pip treats a package already present in the environment as satisfying a requirement — which is what makes --system-site-packages work, and why the cffi that ships with the interpreter is usable at all. uv resolves against the index instead and installs its own copy regardless of what is already there.

So uv venv --system-site-packages creates the environment, and uv then ignores the contents:

$ uv pip install --python .venv/bin/python cryptography
Resolved 3 packages
   Building cffi==2.1.1
  × Failed to build `cffi==2.1.1`
      _configtest.c:1:1: error: thread-local storage is not supported for the
      current target
  help: `cffi` (v2.1.1) was included because `cryptography` (v50.0.0) depends on it

uv found the zopen wheel for cryptography correctly — the failure is entirely cffi, which has no z/OS wheel and cannot be compiled here. Adding UV_CONSTRAINT does not change it, and neither does --system-site-packages. With that flag the environment is left holding the interpreter's cryptography 3.3.2 — the 2021 release with the certificate-validation CVEs — so check the version afterwards rather than trusting the exit status. Without it the install simply leaves no cryptography at all, which at least fails honestly.

Everything without a cffi dependency installs normally.

Python 3.14 needs a recent enough uv

Older uv builds could not use 3.14 at all, failing before the environment was created:

error: Failed to inspect Python interpreter
  Caused by: Unknown operating system: `zos`

The interpreters disagree about what to call this platform, and uv understood only the older spelling:

interpretersysconfig.get_platform()
3.12os390-29.00-8561
3.13os390-29.00-8561
3.14zos

The port now ships a build that recognises both, so 3.12, 3.13 and 3.14 all work. If you see that error, you are on an older binary — reinstall with zopen install uv.

(The same split shows up in wheel filenames — 3.12 and 3.13 build os390_29_00_8561 wheels while 3.14 builds zos ones — which is why the index retags them.)

uv cannot install interpreters

uv python install fetches python-build-standalone builds, which are not published for this platform:

$ uv python install 3.12
error: No download found for request: cpython-3.12-zos-s390x-none

Use the interpreters already on the system; uv python list finds them.

Keep the cache off a small filesystem

uv defaults its cache under $HOME, which is often far smaller than /tmp here. The same warning as for pip applies, and for the same reason — set UV_CACHE_DIR.

Package-specific notes

watchfiles

The default file watcher detects nothing on z/OS. It starts, runs, and never fires — no error. z/OS has no inotify/kqueue equivalent, so the polling backend is the one that works, and it has to be asked for:

sh
export WATCHFILES_FORCE_POLLING=true

zopen install watchfiles sets this in the port's environment. If you install the wheel from the index instead, set it yourself, or pass force_polling=True to watch().

Two separate channels

zopen ships Python packages two ways, and they are not the same catalogue:

pip install from the indexzopen install
what you geta wheel, per interpretera pax, installed into the zopen tree
interpretersthe one you install intoall supported ones share the install
dependenciesresolved by pip from PyPIzopen ports only

Everything on this page concerns the first. zopen install <name> is the second, and it does not consult PIP_CONSTRAINT or the wheel index at all.

When something is missing

If a package you need is not in the index, it either has not been ported or cannot be. Python candidate status records which, measured by actually attempting the install on z/OS rather than inferred from metadata. Contributing Python packages covers the community workflow for adding one, and Porting Python packages covers the build mechanics.

Reporting problems and giving feedback

Report against the port rather than here. Each ported tool and each ported package has its own repository named <name>port, and its issue tracker is what the people maintaining that port actually watch:

what went wrongwhere it goes
a tool misbehaves — uv, pip, an interpreterthat tool's port repo
a wheel is broken, stale, or missing from the indexthat package's port repo
something on this page is wrong or out of datemeta issues
a question, or you cannot tell which of the above it ismeta discussions

So a uv problem goes to uvport and a watchfiles problem to watchfilesporthttps://github.com/zopencommunity/<name>port/issues in general.

Most port repositories have Issues enabled but not Discussions, so anything conversational belongs in meta discussions or the System Z Enthusiasts Discord.

A report is far easier to act on with the output of zopen version, the interpreter (python3 -VV), and the failing command with its full output — the z/OS-specific failures on this page tend to look like ordinary dependency errors, so the raw text matters.

Copyright © zopen community a Series of LF Projects, LLC. For website terms of use, trademark policy, and other project policies, please see https://lfprojects.org.