cyclonedx-python-lib
cyclonedx-python-lib is the OWASP-maintained Python data model for CycloneDX, the software bill of materials standard. It gives you typed classes for the spec (Bom, Component, ComponentType, licences, dependencies, external references), serialisers that write JSON or XML at a chosen schema version, deserialisers that read an existing document back into the model, and optional schema validators. The README is blunt about the scope: this is a library, not a tool. It does not inspect your project or discover dependencies, so if you wanted to point something at a repo and get an SBOM out, the cyclonedx-bom command line tool is the thing built on top of this.
The right foundation if you are writing tooling that speaks CycloneDX, with an unusually careful data model and validators in the same package. If you only want an SBOM for your own project, you want the cyclonedx-bom CLI instead.
Use it if
- You are building a tool that emits CycloneDX documents and want the spec modelled for you rather than assembling dictionaries by hand
- You need to read existing SBOMs, merge or filter them, and write them back out, possibly at a different schema version
- You need output in both JSON and XML from the same in-memory model, which the outputter abstraction handles
- You want schema validation in the same library that produced the document, so a malformed BOM fails in your test suite rather than in a customer's scanner
- You just want an SBOM for your Python project: use cyclonedx-bom, which wraps this library and actually reads your environment or requirements file
- Your organisation standardised on SPDX, since nothing here converts between the two formats
- You cannot absorb frequent major versions: 157 releases have taken it to major 11, and module paths do move between them
- You want to avoid extras juggling, because validation is optional and calling a validator without the right extra installed raises MissingOptionalDependencyException instead of validating
- You do not already know the CycloneDX spec, as the documentation explains the library and assumes the standard is something you bring with you
Setup reality
The base install is small and pure Python, but validation is not included: you need cyclonedx-python-lib[validation] (or the json-validation or xml-validation extras) to pull in jsonschema and lxml, and without them the validators raise MissingOptionalDependencyException, which the shipped examples catch and treat as a skip. Reading XML safely is your job too, since the examples parse with defusedxml rather than the standard library. Two smaller edges: from_json and from_xml are attached to Bom at runtime by the serialisation layer, so mypy needs a type ignore on those calls, and output_to_file defaults allow_overwrite to False and raises FileExistsError rather than replacing an existing file.
Patterns
Build a BOM in memorybuild-bom
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component, ComponentType
bom = Bom()
bom.metadata.component = root = Component(
name='myApp',
type=ComponentType.APPLICATION,
bom_ref='myApp',
)bom_ref is the identifier every dependency edge points at, so set it yourself when you want stable output across runs.
Add a dependency with a package URLadd-component
from packageurl import PackageURL
from cyclonedx.model.component import Component, ComponentType
component = Component(
type=ComponentType.LIBRARY,
name='some-component',
group='acme',
version='1.33.7',
purl=PackageURL('pypi', None, 'some-component', '1.33.7'),
bom_ref='some-component@1.33.7',
)
bom.components.add(component)bom.components is a set, so adding the same component twice is a no-op rather than a duplicate entry.
Attach licences without hand-building objectslicenses
from cyclonedx.contrib.license.factories import LicenseFactory
lc = LicenseFactory()
component.licenses.add(lc.make_from_string('MIT'))
component.licenses.add(
lc.make_from_string('GPL-3.0-only WITH Classpath-exception-2.0')
)The factory decides between an SPDX id, an expression and a named licence for you; note the module moved under cyclonedx.contrib, so older snippets import from elsewhere.
Record who depends on whatdependency-graph
bom.register_dependency(root, [component])
bom.register_dependency(component, [transitive])Components with no registered edges still serialise, they just appear as dependency entries with no dependsOn, which some consumers read as 'no dependencies' rather than 'unknown'.
Serialise to JSON at a chosen schema versionoutput-json
from cyclonedx.output import make_outputter
from cyclonedx.schema import OutputFormat, SchemaVersion
outputter = make_outputter(bom, OutputFormat.JSON, SchemaVersion.V1_7)
print(outputter.output_as_string(indent=2))Serialising to an older schema version silently drops fields that version does not know about, so check the output when you downgrade for a legacy scanner.
Get XML from the same modeloutput-xml
from cyclonedx.output import make_outputter
from cyclonedx.schema import OutputFormat, SchemaVersion
xml = make_outputter(bom, OutputFormat.XML, SchemaVersion.V1_7)
serialized = xml.output_as_string(indent=2)One model, two formats; the round trip through JSON and XML is expected to produce equal Bom objects, and the examples assert exactly that.
Pin the outputter class directlypinned-outputter
from cyclonedx.output.json import JsonV1Dot5
outputter = JsonV1Dot5(bom)
serialized = outputter.output_as_string(indent=2)Explicit classes make the schema version a compile-time fact instead of an argument, which type checkers can then verify.
Write the document to diskwrite-file
outputter.output_to_file(
'sbom.json',
allow_overwrite=True,
indent=2,
)allow_overwrite defaults to False and an existing file raises FileExistsError, which surprises anyone regenerating an SBOM in CI.
Validate before you ship the documentvalidate
from cyclonedx.exception import MissingOptionalDependencyException
from cyclonedx.schema import SchemaVersion
from cyclonedx.validation.json import JsonStrictValidator
validator = JsonStrictValidator(SchemaVersion.V1_7)
try:
errors = validator.validate_str(serialized_json)
if errors:
raise SystemExit(f'invalid SBOM: {errors!r}')
except MissingOptionalDependencyException as error:
print('validation skipped:', error)Install the validation extra or that except branch turns your validation step into a silent no-op.
Pick the validator from the output formatvalidator-by-format
from cyclonedx.validation import make_schemabased_validator
validator = make_schemabased_validator(
outputter.output_format,
outputter.schema_version,
)
errors = validator.validate_str(serialized)Reading both values off the outputter keeps validator and document in step when you change schema version in one place.
Read an existing JSON SBOMdeserialize-json
from json import loads as json_loads
from cyclonedx.model.bom import Bom
bom = Bom.from_json( # type: ignore[attr-defined]
json_loads(json_data)
)from_json takes an already parsed dict, not a string, and the type ignore is in the project's own examples because the method is added dynamically.
Read an XML SBOM safelydeserialize-xml
from defusedxml import ElementTree as SafeElementTree
from cyclonedx.model.bom import Bom
bom = Bom.from_xml( # type: ignore[attr-defined]
SafeElementTree.fromstring(xml_data)
)The examples use defusedxml rather than the standard library parser, which matters when the SBOM came from somewhere you do not control.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cyclonedx-bom | PyPI | You want a command that generates an SBOM from a Python environment or requirements file instead of an API to build one |
| spdx-tools | PyPI | Your compliance process is built on SPDX documents rather than CycloneDX |
| pip-audit | PyPI | The real goal is finding known vulnerabilities in your dependencies, with CycloneDX output as a side effect |