Porting the CMS Outpatient Code Editor to Pure Python: Introducing PyOCE¶
May 7, 2026 — By the Bedrock Billing Engineering Team
If the MS-DRG Grouper is the engine of inpatient reimbursement, the Integrated Outpatient Code Editor (I/OCE) is its outpatient counterpart — and arguably the more complex of the two. Every hospital outpatient claim processed by Medicare passes through the I/OCE before a single dollar is paid. It validates diagnosis and procedure codes, enforces NCCI bundling edits, assigns Ambulatory Payment Classifications (APCs), applies multiple-procedure discounting, resolves composite packaging, and handles dozens of specialty payment paths from FQHCs to biosimilars to telehealth.
For years, the only way to run this logic was through the official CMS Java reference implementation — a heavyweight JAR-based system backed by Protobuf-serialized reference data and in-memory HashMaps. Integrating it into modern Python-based RCM pipelines meant bridging into the JVM via JPype, managing classpath dependencies in containers, and accepting a multi-second startup penalty on every cold boot.
Today we are releasing PyOCE: a complete, production-grade Python port of the CMS I/OCE, verified against the official Java implementation across thousands of CMS TESTDB claims. Install it with pip install pyoce and process your first outpatient claim in five lines of code — no JVM, no Protobuf, no ceremony.
1. Why the I/OCE Is the Hardest Problem in Outpatient Billing¶
The MS-DRG Grouper, which we previously ported to Zig, is fundamentally a classification tree: feed in diagnoses and procedures, walk a decision graph, get back a single DRG number. The I/OCE is an entirely different beast.
A single outpatient claim can carry dozens of line items, each with its own HCPCS/CPT code, revenue code, modifiers, service date, and charge. The I/OCE must:
- Validate every input — bill type, dates, each diagnosis, each line item's HCPCS code, revenue code, and modifiers — against versioned reference tables.
- Apply 150+ sequential edit rules, routed by bill type. A standard 13x (Hospital Outpatient) claim triggers a different rule set than a 76x (FQHC) or 85x (Critical Access Hospital) claim. There are 24 distinct bill-type processing paths.
- Enforce NCCI edits — 1.59 million procedure-to-procedure code pairs that identify services which should not be billed together.
- Assign APCs and status indicators — grouping each service into one of hundreds of Ambulatory Payment Classifications for pricing.
- Apply packaging and discounting — resolving composite APCs, comprehensive APC packaging, multiple procedure discounting, token charge handling, and add-on code pairing across 7 different add-on types.
- Handle specialty logic — FQHC visits, biosimilar bundling, Section 603 site-of-service adjustments, partial hospitalization, intensive outpatient programs, telehealth, non-opioid therapies, corneal transplants, allogeneic transplants, skin substitutes, and more.
This is not a rules engine you can approximate. Hospital outpatient payment accuracy depends on reproducing every decision exactly as CMS specifies it.
2. Architecture: From Protobuf + JVM to LMDB + Pure Python¶
The official Java I/OCE loads all reference data from Google Protobuf binary files into in-memory HashMap structures at startup. This approach works, but it means:
- Slow cold starts. Deserializing 213,000+ records from Protobuf into Java objects takes multiple seconds.
- High memory baseline. Every version range, every HCPCS attribute (130+ flags per code), every NCCI pair lives as a Java object on the heap.
- JVM dependency. Running the engine from Python requires a JNI bridge and a full Java runtime.
PyOCE replaces this entire data layer with LMDB — a memory-mapped key-value store that uses the operating system's virtual memory subsystem to make data available without deserialization.
CMS Flat Files (39 tables, 213K records)
│
▼
build_db.py ─── marshal.dumps() ───▶ pyoce.mdb (LMDB)
│
▼
mmap() into process memory
marshal.loads() per lookup
When PyOCE imports, it inflates a ~2.7 MB compressed LMDB archive to ~/.cache/pyoce/ on first use. From that point on, every reference data lookup is a zero-copy memory-mapped read — no startup penalty, no heap bloat, and full support for concurrent reads from multiple threads and processes.
The 39 Reference Tables¶
PyOCE ships with 39 CMS reference tables covering the complete I/OCE data surface:
| Category | Tables | Records |
|---|---|---|
| Code Data | APC, HCPCS, ICD-10 DX, Revenue, Modifier, CAPC, Discount Formula, Edit Bypass, Non-Opioid Passthrough, Contractor Template | ~189K |
| Mapping Tables | NCCI pairs, 7 add-on code types, biosimilar, composite APC, CAPC pairs, device-procedure, FQHC visit, IMRT, modifier conflict, RHC conflict, Section 603 override | ~22K |
| Offset Tables | APC offset, code-pair offset, HCPCS offset | ~4.6K |
| Description Tables | Edit descriptions, APC names, claim dispositions, status indicators, payment indicators, and more | ~350 |
All tables are version-aware. Lookups respect historical version boundaries so that claims from any I/OCE version range are processed with the correct reference data.
3. The Processing Pipeline¶
Claims flow through a 5-phase sequential pipeline that mirrors the Java implementation exactly:
| Phase | What Happens |
|---|---|
| Initialization | Open LMDB environment, validate reference data availability (cached after first claim) |
| Preprocessing | Convert external dict to internal model, validate bill type / dates / provider IDs, load per-claim table data, validate contractor overrides |
| Processing | Route to 1 of 24 bill-type-specific logic paths, execute 150+ edit rules sequentially |
| Postprocessing | Calculate claim disposition, payment flags, multiple-procedure discounting, composite APC assembly, unit adjustments |
| Finalization | Copy internal processing state back to external output dict |
The processing phase is where the complexity lives. Each bill type path is a precisely ordered sequence of rule classes — we ported 92 specialty logic modules and 24 bill-type routing paths, each verified against the Java reference.
4. What It Looks Like in Practice¶
Basic Claim Processing¶
import pyoce
claim = {
"claimId": "DEMO-001",
"billType": "131",
"dateStarted": "20240101",
"dateEnded": "20240101",
"principalDiagnosisCode": {"diagnosis": "J449"},
"lineItemList": [
{
"serviceDate": "20240101",
"hcpcs": "99213",
"revenueCode": "0510",
"unitsInput": "1",
"charge": "15000",
}
],
}
result = pyoce.process_claim(claim)
print(f"Return code: {result['processingInformation']['returnCode']}")
for li in result["lineItemList"]:
print(f" {li['hcpcs']} → APC {li['paymentApc']}, SI {li['statusIndicator']}")
Plain dict in, plain dict out. No ORM, no protobuf, no factory classes in your application code.
Reusable Engine with Description Lookups¶
For batch processing or interactive applications, the IoceComponent class maintains a warm engine instance and exposes human-readable descriptions for every edit, APC, HCPCS code, status indicator, and revenue code:
engine = pyoce.IoceComponent()
# Process claims through the warm engine
result = engine.process(claim)
# Look up human-readable descriptions
print(engine.get_edit_description("001")) # "The principal diagnosis field is blank..."
print(engine.get_apc_description("5021")) # APC description
print(engine.get_hcpcs_description("99213")) # E/M office visit description
5. Parity: Verified Against the CMS Reference¶
PyOCE is not a reimagining — it is a line-by-line port of the CMS Java I/OCE v27.1. Every class, every method, every conditional branch has a direct counterpart in the Python codebase. The source files are annotated with their Java origins:
We validate parity by feeding the official CMS TESTDB claims through both engines and comparing output field-by-field — claim disposition, every line item's APC assignment, status indicator, payment indicator, discounting formula, rejection/denial flags, packaging flags, edit lists, and modifier outputs.
The integration test suite generates a detailed mismatch report for any divergence:
uv run pytest tests/integration # Full parity suite
uv run python tests/integration/test_parity_report.py --claim <id> # Single claim trace
6. The Scale of the Port¶
To give a sense of the engineering effort:
| Metric | Count |
|---|---|
| Python source files | 375 |
| Lines of Python | 43,700+ |
| Edit rules ported | 150+ |
| Bill-type routing paths | 24 |
| Specialty logic modules | 92 |
| Reference data tables | 39 |
| Data access objects (DAOs) | 36 |
| Total reference records | 213,277 |
| NCCI edit pairs (grouped) | 10,446 groups (1.59M raw pairs) |
| Unit test files | 42 |
Every one of those 375 files maps to a specific component in the CMS Java source tree. The internal model alone tracks ~50 fields per claim and ~40 fields per line item — flags, table data references, processing state, and edit lists.
7. Why Pure Python?¶
When we built mz-drg, we chose Zig for raw speed because MS-DRG grouping is a tight computational loop over binary data. The I/OCE is a fundamentally different problem:
- Logic density over compute density. The bottleneck is branching complexity, not arithmetic throughput. 150+ rules with nested conditionals, cross-line-item interactions, and stateful flag management don't benefit from SIMD or cache-line optimization the way a binary search tree does.
- Maintainability at scale. With 375 source files and quarterly CMS updates, the port needs to be readable, debuggable, and updatable by healthcare engineers — not just systems programmers. Python makes the 1:1 mapping to Java transparent.
- Ecosystem fit. The I/OCE is most commonly called from Python-based RCM pipelines, data analytics platforms, and Django/FastAPI web applications. A native Python library eliminates the serialization boundary entirely.
Even so, PyOCE achieves 1,000+ claims per second on commodity hardware — more than sufficient for real-time claim adjudication, batch analytics, and API serving.
8. Getting Started¶
Requirements: Python 3.12+. The only runtime dependency is lmdb. On first use, the bundled reference database (~2.7 MB compressed) is automatically inflated to ~/.cache/pyoce/.
Then process your first claim:
import pyoce
result = pyoce.process_claim({
"billType": "131",
"dateStarted": "20260101",
"dateEnded": "20260101",
"principalDiagnosisCode": {"diagnosis": "J449"},
"lineItemList": [{
"serviceDate": "20260101",
"hcpcs": "99213",
"revenueCode": "0510",
"unitsInput": "1",
"charge": "15000",
}],
})
9. Open Source, Open Standards¶
PyOCE is released under the Apache License 2.0 and is available at github.com/Bedrock-Billing/pyoce.
Together with mz-drg for inpatient MS-DRG grouping, we now offer a complete open-source Medicare claims processing toolkit — covering both the inpatient and outpatient payment systems — that runs natively in any Python environment without external dependencies, JVM bridges, or proprietary software.
We built these tools because we believe the foundational logic of healthcare payment should be accessible, auditable, and fast. If you are building RCM analytics, claim scrubbing pipelines, EHR integrations, or payer adjudication systems, we hope PyOCE saves you the same months of reverse-engineering that we invested.
Check out the repository on GitHub to explore the codebase, run the parity tests, and help advance open-source healthcare engineering.