Skip to content

Porting the CMS Outpatient Code Editor to Pure Python: Introducing PyOCE

May 7, 2026By 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:

  1. Validate every input — bill type, dates, each diagnosis, each line item's HCPCS code, revenue code, and modifiers — against versioned reference tables.
  2. 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.
  3. Enforce NCCI edits — 1.59 million procedure-to-procedure code pairs that identify services which should not be billed together.
  4. Assign APCs and status indicators — grouping each service into one of hundreds of Ambulatory Payment Classifications for pricing.
  5. 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.
  6. 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:

INITIALIZATION → PREPROCESSING → PROCESSING → POSTPROCESSING → FINALIZATION
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:

"""Port of gov.cms.oce.logic.processing.BillType13xLogicPath."""

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

pip install pyoce

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.

pip install pyoce

Check out the repository on GitHub to explore the codebase, run the parity tests, and help advance open-source healthcare engineering.