mrkeyoor.com_
Fri 07 Aug 22:57 UTC
PyPIUtilsupdated 07 Aug 2026

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.

Verdict

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.

API stability2/5157 releases have reached major version 11 and majors land several times a year; module locations shift between them (the licence factory now lives under cyclonedx.contrib), and the deserialisation entry points are injected at runtime rather than declared, so type checkers need explicit ignores
Docs4/5Read the Docs covers architecture, modelling, schema support and outputting with a generated API reference, and the repository ships runnable serialise and deserialise examples; it documents the library rather than the CycloneDX standard, so newcomers still need the spec open in another tab
Maintenance5/5Maintained under OWASP with the repo pushed 2026-08-07, 11.11.0 released 2026-06-17, an OpenSSF best practices badge, published coverage, and 34 open issues
Ecosystem4/58.3M weekly downloads as the base other CycloneDX tooling builds on, plus a conda-forge build and public Slack and groups.io channels; the 113 stars show how rarely anyone depends on it directly

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
Skip it if

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

PackageRegistryPick it when
cyclonedx-bomPyPIYou want a command that generates an SBOM from a Python environment or requirements file instead of an API to build one
spdx-toolsPyPIYour compliance process is built on SPDX documents rather than CycloneDX
pip-auditPyPIThe real goal is finding known vulnerabilities in your dependencies, with CycloneDX output as a side effect