Pinning fixes a package to one version. A lockfile records the full resolved set, including transitive dependencies. Hash checking goes further and fixes the exact files. Together they make a PyPI install reproducible and let pip refuse anything that differs from what you reviewed.
What is the difference between a pin, a lock and a hash?
Three layers, often mixed up.
- A pin is a requirement such as
requests==2.32.3. It fixes the version of one package. It says nothing about what that package depends on. - A lock is a file produced by a resolver listing every package in the environment with exact versions, direct and transitive.
- A hash is a checksum of a specific downloaded file. A version number can be reused by a repackaged file, or a mirror can serve something different. A hash cannot.
A constructed example: your app requires one package, a web client. The client depends on a URL library, a certificate bundle and a charset detector. You pinned only the client. Next month one of the three transitive packages publishes a new release, and your fresh container build picks it up without any change in your repository. A lock would have held all four at the versions you tested.
How does pip enforce hashes?
pip's documentation describes hash-checking mode. You add a --hash option per requirement in a requirements file, carrying the expected sha256 value, and pip checks downloads against it. It was added in pip 8.0, and it protects against remote tampering and network issues.
Two rules from the docs matter. First, hash checking is all or nothing: specifying a hash on any requirement turns the mode on globally. Second, hashes are then required for all requirements, because a partially hashed file is of little use. The same page also suggests disallowing source distributions with --only-binary :all: for a more secure installation, since building from source runs code during install. It also recommends installing your own project with --no-deps, so that pip cannot quietly fetch something you missed in the requirements file.
You can force the mode with --require-hashes. A failing install is the feature: a mismatch means the file is not the one you reviewed.
What do the newer lock formats add?
A requirements file with hashes is a lock in spirit, but tools have each used their own formats. PEP 751, now final, proposes a standard file format for recording Python dependencies for reproducible installation, aimed at being human readable and machine generated. A conforming lock file is named pylock.toml, or pylock.<name>.toml when you need several. The PEP itself notes it is now a historical document and that the current specification lives on the PyPA specifications page, so read that for details before you build tooling.
Tools also keep their own lockfiles. uv's documentation describes uv.lock: commands that read the lockfile update it automatically if needed. uv run --locked raises an error instead of updating it when the file is out of date, and --frozen uses the lockfile without checking whether it is current. For CI, the strict --locked behaviour is the one you want, because a stale lock should fail the build, not be quietly repaired.
What can still go wrong?
A lock protects against change. It does not decide whether the locked packages were good in the first place. A typosquatted name locked today is locked forever. Pins also age: a pinned version gains a known vulnerability next quarter and nothing in the file tells you. Plan regular update runs, and scan the lock file for known problems.
What to do
- Generate a lock from your resolver and commit it. Never ignore it in git.
- In CI, install in strict mode so a stale or mismatched lock fails the build.
- If you use pip, produce a requirements file with hashes, and install with
--require-hashes. - Prefer wheels with
--only-binary :all:where the project can live with it. - Install your own project with
--no-deps. - Review the lock file diff in every pull request that changes it.
- Schedule dependency updates and scan the lock for known vulnerable and malicious packages.
Safeguard can scan Python lock and requirements files and generate an SBOM from them, and the free scan at gold.safeguard.sh/scan covers up to 100 dependencies in the browser.
Quick check
If I pin every direct dependency, am I locked?
No. Transitive dependencies can still change. A lock lists the whole resolved set, and hashes fix the exact files.
Why does pip require hashes for every requirement?
Because hash checking is all or nothing. A partially hashed file would leave some packages unchecked, so pip enforces it globally.
Sources
- pip documentation: Secure installs
- PEP 751: A file format to record Python dependencies for installation reproducibility
- uv documentation: Locking and syncing
Last reviewed: 18 October 2026.