How to Pin Python Dependencies With pip Hash Checking
How to pin Python dependencies with exact versions and SHA-256 hashes so pip refuses a poisoned release, plus what hash checking cannot protect against.
To pin Python dependencies so a surprise release cannot install itself, list every package and every transitive dependency with an exact == version and at least one --hash=sha256: value in requirements.txt, then install with pip's --require-hashes option. pip refuses any file whose hash does not match. Teams that did this were not hit when the poisoned LiteLLM 1.107.0 wheels appeared on PyPI on 21 October 2026.
Why pin Python dependencies at all?
By default pip installs whatever the index offers that satisfies your version range, and it performs no check against remote tampering. An unpinned or loosely pinned requirement means the next build can pull a release nobody on your team has reviewed.
The LiteLLM supply chain attack is a clean example. The malicious version was live on PyPI for under two hours. Every environment that resolved "latest" in that window got it, and the ones pinned to 1.106.2 did not. The same logic applies to container images: tags such as latest and main-stable were repointed to the bad build, while deployments that referenced an image by digest kept running the image they had tested.
What is pip hash-checking mode?
Hash-checking mode is a pip feature that compares each downloaded file against SHA-256 hashes stored locally in your requirements file and aborts the install on any mismatch. It turns on automatically as soon as any requirement in the file carries a hash, and it is all or nothing for that file.
Once enabled, pip requires a hash for every requirement and every dependency, and each requirement must be pinned with ==, a URL or a file path. Hashes that PyPI puts in download URLs do not count; the pip documentation says those are not a useful guard against tampering because they come from the same place as the file.
How do you write a hash-pinned requirements file?
You write each requirement as an exact version followed by one or more hash options, one per acceptable archive. The pip documentation gives this shape:
FooProject == 1.2 \
--hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \
--hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7
Multiple hashes per package are normal. A project that publishes wheels for several platforms, or both a wheel and a source archive, needs a hash for each file you might install.
Step by step: move a project to hash checking
- Freeze a known-good environment. Start from an environment you trust, built before any incident window you are worried about. Record the exact version of every installed package, including transitive dependencies.
- Collect the hashes. Running an install with
--require-hashesagainst a requirements file that has versions but no hashes makes pip report the hashes of the files it fetched, which is a quick way to bootstrap the list. pip only fetches the preferred archive for each package, so usepip hashon any alternative archive you also need to allow. - Commit the file and review changes. Treat requirements.txt like code. A diff that changes a version or a hash should get the same review as a code change, with a link to the release notes.
- Enforce it in builds. Add
--require-hashesto the install command in your CI and Dockerfiles. Even if someone removes every hash from the file, the build fails instead of silently installing unchecked packages. - Block source builds where you can. The pip documentation recommends
--only-binary :all:so pip never runs a package's build scripts during install. - Install your own project without fetching extras. Use pip's
--no-depsoption for the final project install so it does not pull dependencies that are not in the hashed list:
python -m pip install --no-deps .
What hash checking does not protect against
Hash checking proves you installed the same bytes you approved. It does not prove those bytes were safe. If you add a malicious version to the file yourself, pip will install it faithfully.
That puts the real control at the moment you update a pin. Practical rules that help:
- Do not adopt a new release on the day it ships unless it fixes something you need. A short delay lets registries and researchers catch poisoned uploads.
- Check that a new version matches a tag in the project's official repository and appears in its release notes.
- Watch for lookalike sources. The LiteLLM release was built from
Berri-AI/litellm, not the realBerriAI/litellm.
Calling setuptools directly can also bypass pip's checks and download anything your requirements file missed, so keep installs going through pip.
Pin the images too
Python pins only cover what pip installs. If you run a vendor image, reference it by its digest in deployment manifests rather than by a moving tag, and update the digest through the same reviewed change process. Our GitHub Actions secrets guide covers locking down the CI side of the same pipeline.
Pinning stops the next bad release from reaching you. It does nothing about a key that already leaked, which is the subject of why deleting a leaked API key from Git is not enough. If a malicious package already ran, start with how Python .pth files execute at startup before you run pip on that host again.