When you run pip install numpy, pip usually installs a wheel. A wheel is a ZIP file. It contains Python files, metadata, and machine code that someone else built.
That makes installation fast. It also raises a trust question: who made this file, and can we tell?
Let’s open a real wheel and see what pip chooses.
One version, sixty-six files
NumPy 2.5.4 has 66 files on PyPI: one source distribution and 65 wheels. The wheels cover different Python versions, binary interfaces (ABIs), CPUs, operating systems, and system libraries.
The source distribution is a .tar.gz file containing NumPy’s source files:
$ tar -tzf numpy-2.5.4.tar.gz | head
numpy-2.5.4/.circleci/config.yml
numpy-2.5.4/.clang-format
numpy-2.5.4/.codecov.yml
numpy-2.5.4/.coveragerc
numpy-2.5.4/.ctags.d
numpy-2.5.4/.devcontainer/devcontainer.json
numpy-2.5.4/.devcontainer/setup.sh
numpy-2.5.4/.editorconfig
numpy-2.5.4/.gitattributes
numpy-2.5.4/.github/CONTRIBUTING.md
Its pyproject.toml says that building it requires meson-python and Cython. So an sdist is build input, not a ready-to-install package.
A wheel is the build output. It is a ZIP file arranged in the layout Python expects under site-packages. The example uses CPython 3.14.8, pip 26.2.1, and 64-bit Linux. This is the wheel pip selected:
$ python -m pip install --dry-run --only-binary=:all: \
--report numpy-pip-report.json numpy==2.5.4
Collecting numpy==2.5.4
Downloading numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB)
Would install numpy-2.5.4
By default, pip prefers a matching wheel. If no matching wheel exists, pip builds the sdist.
The --only-binary=:all: option makes this example stricter. It excludes the sdist. Pip must choose a compatible wheel or fail. The report records the exact file pip chose:
{
"name": "numpy 2.5.4",
"download_info": {
"url": "https://files.pythonhosted.org/packages/16/d5/928078d2b28f26829b138b4a6c3980045022fb409f570657a224ae60ef4e/numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl",
"hash": "sha256=d549420b8858885cea8838a727842249218b9c1da24dd517e25c9c7a948310a3"
}
}
So one short command selected one file out of 66. Pip did not download all 66 files.
Opening the wheel
Because a wheel is a ZIP file, the standard archive tools can open it:
$ unzip -l numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl | head
Archive: numpy-2.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Length Date Time Name
--------- ---------- ----- ----
0 10-10-2026 18:13 numpy/
5190 10-10-2026 18:13 numpy/__config__.py
2512 10-10-2026 18:13 numpy/__config__.pyi
49586 10-10-2026 18:13 numpy/__init__.cython-30.pxd
387 10-10-2026 18:13 numpy/__init__.pxd
25220 10-10-2026 18:13 numpy/__init__.py
285616 10-10-2026 18:13 numpy/__init__.pyi
10515 10-10-2026 18:13 numpy/_array_api_info.py
The archive contains 1,044 entries. Twenty are shared libraries:
numpy/_core/_multiarray_umath.cpython-314-x86_64-linux-gnu.so
numpy/linalg/_umath_linalg.cpython-314-x86_64-linux-gnu.so
numpy/random/_generator.cpython-314-x86_64-linux-gnu.so
numpy.libs/libscipy_openblas64_-f48b354e.so
These .so files are shared libraries. They contain machine code, not Python code. Python loads them when NumPy runs, which is one reason NumPy is fast.
The machine code must match the surrounding ABI—the binary interface between compiled pieces. The filename tells us what it expects:
cpython-314: CPython 3.14’s extension ABIx86_64: a 64-bit x86 CPUlinux-gnu: Linux with glibc
manylinux is a standard for Linux wheels that contain native code. The number is the minimum glibc version. For example, manylinux_2_28_x86_64 means 64-bit x86 Linux with glibc 2.28 or newer. The goal is for one wheel to work on many Linux distributions, not only on the machine where it was built.
If any part does not match, importing NumPy can fail or crash. The bundled OpenBLAS library is also machine code, so it must match the same environment.
The difference is now clear. The NumPy sdist contains C, Cython, and Python source. The wheel contains machine code built for one Python version and platform.
The metadata
Every wheel has a .dist-info directory. Pip reads its metadata before installing:
$ unzip -p numpy-2.5.4-*.whl numpy-2.5.4.dist-info/METADATA | head
Metadata-Version: 2.4
Name: numpy
Version: 2.5.4
Summary: Fundamental package for array computing in Python
Author: Travis E. Oliphant et al.
Maintainer-Email: NumPy Developers
License-Expression: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0
...
Requires-Python: >=3.12
This metadata also lists dependencies. This NumPy wheel has no runtime dependencies. Other wheels may list dependencies here. Pip uses those entries to resolve the rest of the install.
The WHEEL file says what kind of wheel this is:
$ unzip -p numpy-2.5.4-*.whl numpy-2.5.4.dist-info/WHEEL
Wheel-Version: 1.0
Generator: meson
Root-Is-Purelib: false
Tag: cp314-cp314-manylinux_2_27_x86_64
Tag: cp314-cp314-manylinux_2_28_x86_64
The tags tell us:
cp314: CPython 3.14- The second
cp314: the Python ABI manylinux_2_27_x86_64: 64-bit x86 Linux with glibc 2.27 or newermanylinux_2_28_x86_64: 64-bit x86 Linux with glibc 2.28 or newerRoot-Is-Purelib: false: the wheel contains native code and is not pure Python
The filename repeats these tags. The long name is a compatibility contract.
Finally, RECORD lists each file and its hash:
numpy/_core/_multiarray_umath.cpython-314-x86_64-linux-gnu.so,sha256=_97L5TgvZJnXf7s_0DT788CsWlUWvnYERhC7QfmTTgQ,10629105
There are 926 entries. 925 have hashes. RECORD does not hash itself.
Tools can use these hashes to check the unpacked files. But a hash only identifies bytes. It does not say the code is safe.
How pip chooses
When pip resolves numpy==2.5.4, it first removes files that do not match. A macOS wheel cannot run on Linux. A cp312 wheel cannot run on CPython 3.14. A Windows wheel does not match this Linux system. The sdist could match, but --only-binary=:all: excludes it.
For this environment, the remaining choice is the wheel shown above. Pip downloads that wheel and unpacks it into the environment. The report also records the SHA-256 digest for that exact URL.
In this path, pip does not compile NumPy. It does not read the C or Cython source. It does not review the machine code. Those steps already happened in CI.
Pip checks compatibility, resolves dependencies, verifies the download, and installs the files.
That is the right job for an installer. It is fast and predictable. Most users do not want pip install numpy to start a compiler.
Who built this file?
The WHEEL metadata says Generator: meson. That names the build backend. It does not name the person or CI job that ran the build.
PyPI’s JSON API reports project ownership and upload time. That tells us who controls the project and when the file was published.
PyPI’s has_sig field is false for this file. That field refers to the older signature mechanism. PyPI also publishes a PEP 740 attestation for this wheel.
The attestation connects the exact wheel digest to a GitHub repository and workflow. The signature is recorded in Sigstore’s transparency log. The publisher information says:
{
"environment": "pypi",
"kind": "GitHub",
"repository": "numpy/numpy-release",
"workflow": "wheels.yml"
}
That is useful evidence. But it is not a security review. It says where the artifact was published from. It does not prove that the workflow was safe, that its dependencies were safe, or that anyone reviewed the machine code.
For that, you need policy. Decide which repositories and workflows to trust. Decide which sources to allow. Decide which checks must pass before an artifact enters your environment.
This is where Fromager starts. It lets an organization choose which packages build from source and which may use prebuilt wheels. It also records those choices.
Building from source is not automatically safe. The next problem is that building a package runs the package’s own build code. But source builds move the decision into a process you can control and audit.
Leave a Reply