To generate an SBOM for a Python project, run an SBOM tool such as the open source cyclonedx-py against the thing you actually deploy: a virtual environment, a pinned requirements file, a Poetry project or a Pipenv manifest. Prefer the resolved, pinned set, because that is what runs.
What input should you feed it?
The cyclonedx-py documentation lists four input types: a Python environment, pip requirements, a Pipenv manifest and a Poetry project. They give different answers.
A requirements file lists what you asked for. If it has loose version ranges, the SBOM reflects the ranges, not the versions that were installed last Tuesday. A virtual environment lists what is installed, which is closest to what ships in a container. A lock file from Poetry or Pipenv records resolved versions without needing an install.
Rule of thumb: generate from the environment you build the release in, after installing from a fully pinned set.
Why do the pins matter?
An SBOM that says requests >= 2 is a wish. One that says a specific version is evidence. The pip documentation describes hash-checking mode, where each requirement carries hashes, installation fails if a file does not match, and all requirements must then be pinned. Using that mode also makes the SBOM trustworthy, because the install step verified what the SBOM names.
A constructed example
A data team ships a notebook server image. Their requirements file pins top-level packages only. Transitive packages float, so two builds a month apart carry different libraries. They switch to a compiled, fully pinned file with hashes, install it in the build image, and run the generator against the environment. The SBOM now lists every package, including transitive ones, with versions that match the running container.
What can go wrong?
Dev tools leak in. If you generate from a developer machine, test frameworks and linters appear as if they shipped. Generate from the production install only.
System libraries are missing. A Python SBOM lists Python packages, not the operating system packages in the base image. Scan the image separately and merge views if you need the whole picture.
Optional extras change the set. Note which extras you installed.
How do you keep the SBOM honest over time?
An SBOM is a snapshot. Dependencies change with every merge, so generate it in the release pipeline rather than by hand, and name the file after the release it describes. Diffing two SBOMs is a cheap review step: a new transitive package appearing in a patch release deserves a look. If you ship a container, also keep the image digest next to the SBOM so the pair can be matched later.
How to check or apply this
- Produce a fully pinned requirements file with hashes, from your resolver of choice.
- Install it in a clean environment in CI with hash checking on.
- Run the generator against that environment and write CycloneDX output.
- Attach the file to the release and record the Python version.
- Scan the SBOM for known vulnerabilities on each build.
- Compare it with the previous release's SBOM to see what changed.
- Add the OS and base image to your inventory by scanning the container separately.
Safeguard ingests SBOMs and scans dependencies, and its CLI can run the check on each build.
Quick check
Should I generate the SBOM from requirements.txt or the environment?
From the installed environment, built from a pinned file, when you can. A requirements file shows what you asked for, not always what ran.
Does a Python SBOM cover the operating system packages?
No. It lists Python packages. Scan the container image to cover system libraries.
Sources
Last reviewed: 18 October 2026.