oxedi
A fast X12 EDI parser for Python, written in Rust. Today it reads the 835 (electronic remittance advice).
- Lossless. Every byte of the file is kept, so
write()returns the file exactly as it was read. - Ready-made tables. Payments, claims, service lines and adjustments come out as typed tables that open in Polars, pandas, pyarrow or DuckDB without copying the data.
- Problems are reported, not raised. A file that is valid EDI but has questionable data still parses; each issue is returned as a diagnostic that names the rule, the position and the value.
- Large files.
streamyields one transaction at a time, so memory stays bounded. - Extensible. Add segments or table columns with a small JSON patch, without touching the code.
Install
pip install oxedi # no Python dependencies
pip install "oxedi[polars]" # with Polars
pip install "oxedi[pandas]" # with pandas and pyarrow
Requires Python 3.11 or later. Wheels are available for Linux (x86_64 and aarch64, plus musl on x86_64), macOS (Intel and Apple silicon) and Windows (x86_64).
Quick start
import oxedi
result = oxedi.parse_file("remittance.835")
claims = result.tables["claims"].to_polars() # or .to_pandas()
services = result.tables["services"].to_polars()
print(result.count_claims(), result.sum_payments()) # sum_payments() is a Decimal
print(result.payer["name"], result.payee["name"])
for diagnostic in result.diagnostics:
print(diagnostic)
oxedi.parse(data) does the same from bytes. A file that is not an 835 at all (for
example, one that does not start with an ISA segment) raises oxedi.ParseError.
Tables
| Table | One row per | Linked by |
|---|---|---|
payments |
payment (transaction) | — |
claims |
claim | payment |
services |
service line | payment, claim |
adjustments |
claim or service adjustment | payment, claim, service |
provider_adjustments |
provider-level adjustment | payment |
result.tables.keys() lists them and table.columns lists a table's columns. Amounts are
decimals, dates are dates, and text fields are kept as the raw bytes from the file (Arrow
binary), so nothing is lost to an encoding guess. Tables follow
the Arrow PyCapsule interface, so any Arrow-aware library reads them directly:
import duckdb, polars as pl
claims = result.tables["claims"]
duckdb.sql("select claim_status, sum(payment_amount) from claims group by 1")
pl.DataFrame(result.tables["services"])
Large files
from pathlib import Path
for batch in oxedi.stream(Path("big.835").read_bytes()):
services = batch.tables["services"].to_polars()
Each batch holds the tables and diagnostics of one transaction. Pass by="2100" (or any
other loop id) to get one batch per claim instead. Parsing releases the GIL, so several
files can be parsed in parallel from threads.
Writing the file back
data = Path("remittance.835").read_bytes()
assert oxedi.parse(data).document.write() == data
Writing
oxedi.write turns tables back into an 835: the tables of a parse, or your own Arrow, Polars or
pandas tables with the spec's columns. Parse, change a value, write:
import datetime
import polars as pl
import oxedi
result = oxedi.parse_file("remittance.835")
frames = {name: result.tables[name].to_polars() for name in result.tables.keys()}
frames["payments"] = frames["payments"].with_columns(trace_number=pl.lit("CHK100235"))
envelope = oxedi.Envelope(
sender_id="ACMEPAYER",
receiver_id="SUNRISECLINIC",
date=datetime.date(2024, 1, 10),
time=datetime.time(9, 0),
usage_indicator="T", # "P" (production) is the default
)
data = oxedi.write(frames, envelope) # bytes, one interchange
The spec is the one that parsed the tables, else the built-in 5010 spec. A dictionary of frames,
as above, does not carry the parse's spec, so pass the one that matches the file's version: for
a 4010 file, oxedi.write(frames, envelope, spec=oxedi.Spec.builtin("4010")); otherwise it is
written as 5010 and refused. A table left out has no rows and a column left out is null. Rows nest by their payment, claim and service columns, the row
number of their parent. Only what the tables hold is written: segments no column reads (for
example the payer's PER*CX or the bank details of BPR) are left out. To reproduce a parsed
file byte for byte, use result.document.write() instead.
Envelope. oxedi.Envelope gives what the tables cannot: sender_id and receiver_id with
their qualifiers (sender_qualifier, receiver_qualifier, default "ZZ"), date and time,
usage_indicator, the first control_number (default 1), optional application_sender and
application_receiver for the group header, delimiters (default *, :, ~ and repetition
^) and line_break (a line break after each segment). The writer works out the rest: the
fixed-width ISA, the control numbers that must match (ISA13/IEA02, GS06/GE02,
ST02/SE02) and the counts (SE01, GE01, IEA01).
Strict by default. The written file is read back with the spec, and every diagnostic of that
read is a finding, as is anything the writer cannot place: for example a required element or
occurrence without a value, a value outside its code list or length, a row whose parent does not
exist or that comes out of order, a value holding a delimiter, or money that does not balance.
A value holding a delimiter is reported first: the file then splits where the data does not, so
it is not read back, and the other findings show once that value is fixed. Any finding raises
oxedi.WriteError, a ValueError whose message lists every finding and whose findings holds
them as oxedi.WriteFinding (each names the table, row and column, or the envelope field, with
the diagnostic behind it); nothing is written. With allow_findings=True the call returns
(data, findings) instead, for example to produce invalid files for tests:
try:
data = oxedi.write(frames, envelope)
except oxedi.WriteError as error:
for finding in error.findings:
print(finding.table, finding.row, finding.column, finding)
data, findings = oxedi.write(frames, envelope, allow_findings=True)
The writer never changes money: the balancing rules (BPR02 against the claims' payments and
the PLB adjustments, each claim's and each service's charge minus payment against their CAS
adjustments) are checked, never used to fill a total. The claim rule does not model interest
(AMT*I): a claim whose payment includes interest does not balance and is refused unless
allow_findings is set.
Null and empty text. In text columns, null is an element the segment does not have and ""
one it has but leaves empty. A null cell in the middle of a segment is still written as an empty
element, so it reads back as ""; only trailing nulls read back as null. The tables of a
parse write back to the same tables.
Extending the spec
The structure of the 835 and the columns of each table are defined by a JSON spec. A patch in JSON Merge Patch format (RFC 7386) adapts it to a payer's variations:
spec = oxedi.Spec.builtin().patch({
"tables": {"claims": {"columns": {
"contract_class": {"segment": "REF", "where": {"1": "CE"}, "element": 2}
}}}
})
result = oxedi.parse(data, spec=spec) # claims now has a contract_class column
Objects merge key by key, while arrays are replaced whole. A loop's segments are named
occurrences, so a patch adds one (or changes or removes it with null) by its name:
{"loops": {"1000A": {"occurrences": {"xx": {"segment": "XX", "pos": 11400}}}}}
pos orders the occurrences of a loop. The transaction and every loop below it share one
position space: the built-in spec numbers a segment of the transaction's n-th table at
n × 10000 plus its implementation-guide position (1000A's N1 is 10800, 2100's CLP 20100), and
a child loop's occurrences sit at their own positions inside that space. The occurrence a
loop opens on comes first: every other occurrence of the loop has a higher pos.
A column reads an element of a segment chosen by segment and optional where
conditions, or of a named occurrence ({"occurrence": "patient_name", "element": 3}). loop
reads a loop inside the table's anchor or above it: in the services table,
{"loop": "2100", "occurrence": "claim_payment_information", "element": 1} gives each service
its claim's id. When an occurrence repeats, pick chooses "first" (the default), "last" or
the n-th match, counting from 1, among the segments read while the row's loop instance is
open; a loop above the anchor offers the segments its enclosing instance read before the row's
instance opened.
parse and stream both accept spec=.
Spec versions
parse, parse_file and stream read the version a file declares (ISA12 and GS08) and use
the matching built-in spec: 5010 (005010X221A1) by default, 4010 (004010X091A1) for 4010
files. Pass spec= to override, or pick one yourself:
spec = oxedi.Spec.builtin(version="4010")
Validating with pyx12
pip install "oxedi[pyx12]" adds pyx12's
implementation-guide validation. parse never calls it; validate does, on demand:
import oxedi
from oxedi.pyx12 import validate
result = oxedi.parse(data)
findings = result.diagnostics + validate(data) # bytes, a path or a binary file object
for d in sorted(findings, key=lambda d: d.level):
print(d.origin, d.code, d) # "oxedi" or "pyx12"; pyx12's own code
if d.segment is not None:
start, end = result.document[d.segment].span # the segment's bytes in your file
validate returns oxedi.Diagnostic, the type parse returns, so the two lists mix, sort
by level and filter by origin. A pyx12 finding has kind == "External",
origin == "pyx12", code set to pyx12's error code and an empty path (pyx12 does not report
the loop); interchange, group and transaction findings are level 1, segment and element
findings level 2. A file pyx12 cannot read, or an exception inside pyx12, comes back as one
level 1 finding with no code whose rule starts with could not finish validating,
instead of a traceback.
Coming from another library
Coming from edi-835-parser? See the migration guide.
Versioning
The project follows semantic versioning. While the version is 0.x, a minor release may change the API. See the changelog.
License
MIT. See LICENSE and THIRD_PARTY_NOTICES.
Metadata
Release files for oxedi 0.4.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 | |
|---|---|---|---|
| oxedi-0.4.0.tar.gz | 338.7 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| oxedi-0.4.0-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details |
| oxedi-0.4.0-cp311-abi3-musllinux_1_2_x86_64.whl | CPython 3.11 | abi3 | Linux musl 1.2+ x86-64 | Details |
| oxedi-0.4.0-cp311-abi3-manylinux_2_28_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| oxedi-0.4.0-cp311-abi3-manylinux_2_28_aarch64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| oxedi-0.4.0-cp311-abi3-macosx_11_0_arm64.whl | CPython 3.11 | abi3 | macOS 11.0+ ARM64 | Details |
| oxedi-0.4.0-cp311-abi3-macosx_10_12_x86_64.whl | CPython 3.11 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 8.0 MB
Release files / oxedi-0.4.0.tar.gz
| Download URL | oxedi-0.4.0.tar.gz |
|---|---|
| Size | 338.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eed9f7913da67695d627ebb200a6166aa073138669364fd3ea6ac04c438466da
|
|
BLAKE2b-256 checksum How to use checksums |
2d7d3a71585e71194ebfc735b90eb94c5b6dd9458ee2c343905fca6114c475c2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / oxedi-0.4.0-cp311-abi3-win_amd64.whl
| Download URL | oxedi-0.4.0-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 1.1 MB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
a04ae1dfa164ef45ca90029857c2740d1560f90562e59ced1c98ef4cb6b252a6
|
|
BLAKE2b-256 checksum How to use checksums |
6391040b8b6212a4e22995ea84899f4121d6d09a42d4c36e9e5205d321ae66fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / oxedi-0.4.0-cp311-abi3-musllinux_1_2_x86_64.whl
| Download URL | oxedi-0.4.0-cp311-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 1.6 MB |
| Tags | CPython 3.11 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
6884a3b0a8f849db17801cd2da89c974b56547aaf51748bf36af1f0aab959dc0
|
|
BLAKE2b-256 checksum How to use checksums |
a4dcd5dd48fe5a5548fdb637ccc1a26f06eb081fefe10dd23df242013e9074d4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / oxedi-0.4.0-cp311-abi3-manylinux_2_28_x86_64.whl
| Download URL | oxedi-0.4.0-cp311-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | CPython 3.11 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
2a40ec89a0af6e870adcddfd5774f4efcfce1e170c9adc661a784ce42cfd9465
|
|
BLAKE2b-256 checksum How to use checksums |
1b066a532fea2796b2fd7fc6a3168728f2b869c45256f206c341962dd1fe5ef2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / oxedi-0.4.0-cp311-abi3-manylinux_2_28_aarch64.whl
| Download URL | oxedi-0.4.0-cp311-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | CPython 3.11 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
f09db63bbd9307ff01d01e8be36a7f7d373264248b5c7ce7f7cbd28d19093a07
|
|
BLAKE2b-256 checksum How to use checksums |
752b7db581594b3ad447ffcd06b51cec75ca392f216738055143fe2a96900488
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / oxedi-0.4.0-cp311-abi3-macosx_11_0_arm64.whl
| Download URL | oxedi-0.4.0-cp311-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.11 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
fa8e8812539b7733fbd6779dca5da4922b6c0d0c86a597c3f9b7afe46ff458b9
|
|
BLAKE2b-256 checksum How to use checksums |
b4db10692b5bba09c795088b085018b47a6886ca11e02d6271ca770fa8a2b814
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / oxedi-0.4.0-cp311-abi3-macosx_10_12_x86_64.whl
| Download URL | oxedi-0.4.0-cp311-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.2 MB |
| Tags | CPython 3.11 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
27eb40d375100017a054ed8c731fdb91e29e13c1b3cfe613e47b9a2ff535f472
|
|
BLAKE2b-256 checksum How to use checksums |
d6fe030d4c0d8a28c8163431800bcdbd2b6610aeed55ba432b672b0670a5e896
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log