Your first SBOM

In this tutorial you build a small package with an SBOM inside its wheel, look at what the SBOM says, and check it the way a CI job would. It takes about ten minutes. You need Python 3.11 or later, git, and uv; any other build frontend, such as python -m build, works the same.

Create the project

Make a directory with a package, a license and a pyproject.toml. The project uses hatchling, the backend python-sbom has the most to say about; Supported backends lists the others.

$ mkdir greet && cd greet
$ mkdir -p src/greet && touch src/greet/__init__.py
$ printf 'Copyright (c) 2026 You\n\nMIT terms\n' > LICENSE
pyproject.toml
[build-system]
build-backend = "hatchling.build"
requires = [ "hatchling>=1.28", "python-sbom[hatch]>=0.3" ]

[project]
name = "greet"
version = "1.0"
description = "says hello"
license = "MIT"
license-files = [ "LICENSE" ]
requires-python = ">=3.11"
classifiers = [
  "Programming Language :: Python :: 3 :: Only",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
  "Programming Language :: Python :: 3.13",
  "Programming Language :: Python :: 3.14",
  "Programming Language :: Python :: 3.15",
]
dependencies = [ "packaging>=24" ]
urls.Source = "https://github.com/you/greet"

[tool.hatch]
build.hooks.python-sbom = {}

[tool.python-sbom]
supplier = { name = "You", url = [ "https://github.com/you" ] }

The empty [tool.hatch.build.hooks.python-sbom] table turns the hook on. [tool.python-sbom] holds python-sbom’s own options; here it names you as the supplier. docs/examples/hatchling holds the finished project.

Commit it, with the build output ignored, so the SBOM can name the exact source revision. A checkout with uncommitted or untracked files has no commit that reproduces it, and then the SBOM names none:

$ printf 'dist/\n*.cdx.json\n' > .gitignore
$ git init --quiet && git add . && git commit --quiet --message first

Build the wheel

$ uv build
Successfully built dist/greet-1.0.tar.gz
Successfully built dist/greet-1.0-py3-none-any.whl

List the wheel’s .dist-info and you find the SBOM next to the metadata:

$ unzip -Z1 dist/greet-1.0-py3-none-any.whl | grep dist-info
greet-1.0.dist-info/METADATA
greet-1.0.dist-info/WHEEL
greet-1.0.dist-info/licenses/LICENSE
greet-1.0.dist-info/sboms/greet.cdx.json
greet-1.0.dist-info/RECORD

The sdist holds a .python-sbom-revision file with your commit. uv built the wheel from that sdist, not from your checkout, and the file is how the wheel still knows which commit it came from.

Read the SBOM

Print a few parts of the document:

$ unzip -p dist/greet-1.0-py3-none-any.whl greet-1.0.dist-info/sboms/greet.cdx.json > greet.cdx.json
$ python - <<'EOF'
import json
import pprint
sbom = json.load(open('greet.cdx.json'))
root = sbom['metadata']['component']
print(root['name'], root['version'], root['licenses'])
print([component['name'] for component in sbom['components']])
print([reference['url'] for reference in root['externalReferences'] if reference['type'] == 'vcs'])
pprint.pp(sorted(tool['name'] for tool in sbom['metadata']['tools']['components']), compact=True, width=100)
EOF

It prints:

greet 1.0 [{'expression': 'MIT', 'acknowledgement': 'declared'}]
['packaging']
['https://github.com/you/greet', 'https://github.com/you/greet/tree/<your commit>']
['cpython', 'hatchling', 'installer', 'packageurl-python', 'packaging', 'pathspec', 'pluggy',
 'python-sbom', 'tomlkit', 'trove-classifiers', 'zipremove']

That is four things:

  • the project itself, with its license;

  • packaging, the runtime dependency the project declares, without a version, since the installer picks it later;

  • a link to your repository at the exact commit you built;

  • the build tools: the interpreter, hatchling, python-sbom and every other package of the build environment.

What the SBOM records says what each part is for.

Check it

Install the command line with its validate extra and check the wheel:

$ uvx --from 'python-sbom[validate]' python-sbom validate dist/ --allow-license MIT
SBOM in greet-1.0-py3-none-any.whl is valid

That is the check a CI job runs before publishing; Check SBOMs in CI shows how to wire it in.

Next steps