ubl-bindings
Python data bindings for all UBL document types. The classes are generated from the official XSD schemas. Use them to read, write and validate UBL documents.
Status: alpha. The API can change.
Why this library
- All document types. UBL 2.1 has 65 document types and UBL 2.5 has 101. All of them have classes, not only the Invoice and the Credit Note. This includes, for example, Order, Despatch Advice and Catalogue.
- All current releases. UBL 2.1, 2.2, 2.3, 2.4 and 2.5 are supported. To add a release, generate its code and run the tests.
- Complete. Each element and attribute of the schemas has a field in a class. Data that a hand-written model does not know is not lost.
- Tested with the official examples. Each example document of each release is parsed, exported, validated and compared with the input. The examples cover about half of the document types, for example 56 of 101 in UBL 2.5.
- XSD validation included. The schemas are part of the package.
- No profile rules. The library does not add the rules of Peppol, XRechnung or EN 16931. You can build such rules on top of it.
The classes are close to the schemas. Thus the API is more verbose than the API of a hand-written model for one document type.
Supported UBL releases
| Release | Status | Document types | Python package | Specification |
|---|---|---|---|---|
| 2.1 | OASIS Standard | 65 | ubl_bindings.v2_1 |
UBL-2.1.html |
| 2.2 | OASIS Standard | 81 | ubl_bindings.v2_2 |
UBL-2.2.html |
| 2.3 | OASIS Standard | 91 | ubl_bindings.v2_3 |
UBL-2.3.html |
| 2.4 | OASIS Standard | 93 | ubl_bindings.v2_4 |
UBL-2.4.html |
| 2.5 | OASIS Standard | 101 | ubl_bindings.v2_5 |
UBL-2.5.html |
UBL 2.0 has no package of its own. Each release from 2.1 on can read UBL 2.0 documents. The tests do this with the official 2.0 examples. To write a UBL 2.0 document, do not use elements that were added after 2.0. validate() cannot do a check against the 2.0 schemas.
UBL 1.0 is not supported. It uses different namespaces and a different structure. See ADR 0004.
Installation
pip install ubl-bindings
The library supports Python 3.10 and later.
Usage
All functions take and return XML as bytes. Reading and writing files is up to you.
Read a document
from pathlib import Path
import ubl_bindings
xml = Path("invoice.xml").read_bytes()
invoice = ubl_bindings.parse(xml)
print(invoice.id.value) # "TOSL108"
print(invoice.legal_monetary_total.payable_amount.value) # Decimal("729")
parse() returns an instance of the class for the root element, for example Invoice or Order. By default it uses the latest release. Each release can read the documents of all older 2.x releases.
To use a specific release:
invoice = ubl_bindings.parse(xml, release="2.1")
A new version of this library can add a new UBL release. Then the default release changes, and parse() returns classes from a different package. If your code depends on the classes of one release, give release=.
The first call of parse() for a release takes about a second, because it imports the generated classes of the release. Later calls take about 30 ms for a typical invoice.
Read a document that is not valid
By default, parse() is strict. It raises ValueError if the document does not fit the classes, for example if a mandatory element is missing or an element is unknown. Strict parsing is not a full validation. For example, it accepts elements in the wrong order. Use validate() to find all errors.
To read as much as possible from a document that is not valid, for example a received invoice, use best-effort mode:
invoice = ubl_bindings.parse(xml, strict=False)
In best-effort mode:
- A missing mandatory element or attribute is
None. - Unknown elements and attributes are skipped. Their data is lost.
- A value that cannot be converted, for example the amount
abc, stays a string. xsdata shows aConverterWarning.
Write a document
from decimal import Decimal
from xsdata.models.datatype import XmlDate
import ubl_bindings
from ubl_bindings.v2_5 import (
AccountingCustomerParty,
AccountingSupplierParty,
Id,
Invoice,
InvoiceLine,
IssueDate,
Item,
LegalMonetaryTotal,
LineExtensionAmount,
PayableAmount,
)
invoice = Invoice(
id=Id(value="INV-1"),
issue_date=IssueDate(value=XmlDate(2026, 11, 2)),
accounting_supplier_party=AccountingSupplierParty(),
accounting_customer_party=AccountingCustomerParty(),
legal_monetary_total=LegalMonetaryTotal(
payable_amount=PayableAmount(value=Decimal("100.00"), currency_id="EUR"),
),
invoice_line=[
InvoiceLine(
id=Id(value="1"),
line_extension_amount=LineExtensionAmount(value=Decimal("100.00"), currency_id="EUR"),
item=Item(),
),
],
)
xml = ubl_bindings.to_xml(invoice)
to_xml() returns UTF-8 encoded bytes with the usual prefixes cac, cbc and ext. To write a file, use Path("invoice.xml").write_bytes(xml).
Validate a document
errors = ubl_bindings.validate(xml) # [] if the document is valid
The validation uses the XSD schemas only. It does not check business rules.
Find the release of a document
All UBL 2.x releases use the same XML namespaces. A document can state its release in the optional cbc:UBLVersionID element:
ubl_bindings.detect_version(xml) # "2.1", or None if the element is missing
Export changes the form of some values
The value stays the same, but the text can change. For example, 12:00:01.000 becomes 12:00:01 and 0 becomes false. Thus an XML signature on an exported document is not valid. To check a signature, use the original bytes.
Export also removes the xsi:schemaLocation attribute. This attribute is only a hint for tools.
Out of scope
- Business rules that are not in the XSD schemas, for example EN 16931, XRechnung or Peppol Schematron rules.
- Other syntaxes, for example UN/CEFACT Cross Industry Invoice (CII).
- Conversion between UBL releases.
Development
uv sync # make the environment
uv run pytest # run the tests
uv run ruff format # format the code
uv run ruff check # find lint errors
uv build # make the wheel and the source distribution
The ruff configuration is in pyproject.toml. See ADR 0005.
Versioning
We use Semantic Versioning. A renamed generated class or field is a breaking change. A new UBL release is a minor change. See ADR 0006.
To make a release:
- Run
uv version --bump major|minor|patch. - Move the entries under "Unreleased" in
CHANGELOG.mdto a section for the new version. - Commit, then add the tag
vX.Y.Z. - Push the commit and the tag. The release workflow checks that the tag is the version in
pyproject.toml, then publishes the package to PyPI.
Layout
ubl-bindings/
├── src/ubl_bindings/
│ ├── __init__.py # parse, validate, to_xml, detect_version (hand-written)
│ ├── schemas/
│ │ ├── 2.1/xsd/ # XSD files from the official release ZIP
│ │ ├── ...
│ │ └── 2.5/xsd/
│ ├── v2_1/ # generated code for UBL 2.1
│ ├── ...
│ └── v2_5/ # generated code for UBL 2.5
├── .xsdata.xml # generator configuration
├── scripts/
│ └── generate.py # runs the generator for one or all releases
├── tests/
│ └── examples/ # example documents from each release ZIP
└── docs/adr/ # architecture decision records
Rules for this layout:
- Code in
src/ubl_bindings/v*/is generated. Do not edit it by hand. Change the generator configuration, then generate again. - The schemas are part of the package, because
validate()reads them at runtime. We keep only thexsd/folder of each release ZIP.
Code generation
We use xsdata to make Python dataclasses from the XSD schemas. See ADR 0001. xsdata makes one module for each XML namespace. All document types of a release share these modules.
The version of xsdata in the dev dependency group is pinned. To update it, change the pin, generate the code for all releases and review the diff.
uv run scripts/generate.py 2.5 # generate the code for one release
uv run scripts/generate.py # generate the code for all releases
Add a new UBL release
- Download the release ZIP from
https://docs.oasis-open.org/ubl/os-UBL-<release>/UBL-<release>.zip. - Copy the
xsd/folder of the ZIP tosrc/ubl_bindings/schemas/<release>/xsd/. - Copy the
xml/folder of the ZIP totests/examples/<release>/. - Add the release to
RELEASESinsrc/ubl_bindings/__init__.py. - Run
uv run scripts/generate.py <release>. - Run
uv run pytest. - Add the release to the table in this README.
Tests
The main test is a round trip over the official examples. For each release and each example of the same or an older release, the test does these steps. The UBL 2.0 examples count as an older release.
- Parse the example.
- Export the result to XML.
- Validate the output against the XSD schemas of the release.
- Compare the output with the input. The comparison ignores differences in the lexical form of a value and the
xsi:schemaLocationattribute.
Each new release brings its own examples. Thus the test suite grows without new test code.
Other tests:
tests/test_api.pytests the public functions.tests/test_readme.pyruns the example in "Write a document" and validates the result.
The CI workflow runs the tests on the oldest supported and the latest Python version. It also generates the code for all releases again and fails if the result is not the committed code.
License
The code of this project is licensed under the MIT License.
The UBL schemas, the UBL examples and the code generated from them come from OASIS, W3C and ETSI. Their own licenses apply. See NOTICE.
"OASIS", "UBL" and "Universal Business Language" are trademarks of OASIS, the open standards consortium where the UBL specification is owned and developed. UBL is a copyrighted © work of OASIS Open. All rights reserved. This project is not an official OASIS product.
Metadata
Release files for ubl-bindings 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ubl_bindings-0.2.0.tar.gz | 2.8 MB | Without provenance | Details for ubl_bindings-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ubl_bindings-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for ubl_bindings-0.2.0-py3-none-any.whl |
Total release size: 7.0 MB
Release files / ubl_bindings-0.2.0.tar.gz
| Download URL | ubl_bindings-0.2.0.tar.gz |
|---|---|
| Size | 2.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2dbea6081af85e4e7e884bcf711555038c0f09b31761ee07d189b35e44c4d16e
|
|
BLAKE2b-256 checksum How to use checksums |
d5187e116ebb75d576620d0c5d87b3ec9aeac9fa3a2c1044fd49812263518e2c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ubl_bindings-0.2.0-py3-none-any.whl
| Download URL | ubl_bindings-0.2.0-py3-none-any.whl |
|---|---|
| Size | 4.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9cc840c6cb5aa5d743e999ca5b34b72a46c94725ccbb8a92426cadc3f1f2ee8d
|
|
BLAKE2b-256 checksum How to use checksums |
74a539ba3fe139041cbf5617d423d220f6656d1fd6eb7d5ae34da7bbb43bf957
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for ubl-bindings 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.