Treat unpinned requirements files as approximate. Pip chooses versions when it installs, so a scanner reading "requests" with no version cannot know what was deployed. Resolve once in a clean environment, save the result as pinned versions, scan that, and report which directories were pinned and which were not.
Why unpinned files are a problem for scanning
Pip's resolver does real work at install time. Its documentation states that the resolver is capable of backtracking, meaning it can discard a choice and try an earlier version to satisfy all constraints. The same file can therefore yield different installs on different days, as new releases appear.
For a scanner this means a manifest lists intent, not fact. Two scans a week apart can disagree without any code change.
Steps
- Find the files. Search for requirements files, pyproject.toml and setup.py in each service directory. Legacy repositories often hold several requirements files per service, such as base, dev and prod.
- Pick the one that production uses. If you cannot tell, ask the owning team or read the Dockerfile or deployment script.
- Build a clean virtual environment with the Python version production uses, then install from that file.
- Export the installed versions to a pinned file.
- Scan the pinned file and compare with a scan of the original. Note any package that differs.
- Commit the pinned file beside the original, or in place of it if the team agrees.
A simple export command, run inside the clean environment:
pip freeze > requirements.pinned.txt
Where the answer differs
| Case | Handling |
|---|---|
| Production image still available | Read installed packages from the image; that is the real answer |
| Only the file exists | Resolve in a clean environment; label as reconstructed |
| Pinned with hashes already | Scan as is; this is the best case |
| Multiple Python versions in use | Resolve once per version; results may differ |
The first row is worth remembering: if a built container image exists, it holds the true list. Compare the image with your reconstructed file to see how far apart they are.
Keep reports honest
In the coverage map, add a column for pin status: exact, reconstructed or unpinned. When you present the first scan to leadership, show how much of the result rests on reconstruction. It prevents a later argument when a deployed version turns out to differ.
Avoid a common trap
Do not fix unpinned files by pinning to whatever pip installs today in a service nobody runs tests for. A newer resolution can break the service. For frozen services, leave the file alone and record the limit instead.
Where Safeguard fits
Safeguard's dependency scanning can read pinned and unpinned files and its SBOM output records what was found, and reachability analysis helps decide which findings matter. Try the free scan at https://gold.safeguard.sh/scan.
Quick check
Are hashes needed for scanning?
No. Hashes help integrity checks at install time. For scanning, exact versions are what matter.
Should I pin every transitive package?
For services you rebuild, yes, because a freeze captures them. For frozen services, record the limit instead of changing the file.
Sources
Last reviewed: 19 October 2026.