changelog · 2026-01-22 · 4 min read

v2.3.0: table cells you can trust, and pipeline retries

This release is mostly about the ugly parts of document pipelines: partial failures and tables with merged cells. The fun features can wait.

v2.3.0 is live on api.unstructred.io and on PyPI. Upgrade with pip install -U unstructred. Python 3.9 through 3.13 supported; the SDK no longer supports 3.8, which had a good run and is now someone else's problem.

Cell-level table provenance

Table extraction now returns cell-level provenance: every cell carries its bounding box and source page, and row spans / column spans that previously split into ghost rows are preserved as real structure.

Why it matters, concretely: one customer's audit workflow required proving which pixels a given total came from. Before v2.3.0 we could prove the page; now we can prove the cell. That's the difference between "trust us" and "here's the receipt," and auditors strongly prefer the second.

for row in doc.tables.line_items.rows:
    for cell in row.cells:
        print(cell.value, cell.bbox.page, cell.bbox.rect)
        # 'Netto' 1 [112.4, 688.0, 241.2, 703.1]

The old flat representation is still returned (row.values) so existing code doesn't break — the new structure is additive.

Pipeline retries, per page

Previously, if OCR timed out on page 34 of a 212-page contract, the whole document failed and the pipeline retried from the top. Every customer with long documents hated this, correctly. Now: pages 1–33 are kept, page 34 gets a structured retry record, and your webhook still fires — with a partial: true flag and the failed page ranges listed. You decide whether to re-run just those pages or proceed with what you have.

Behavior change: pipelines that used to fail hard on partial OCR errors now succeed with partial: true. If your consumer treats any successful parse as complete, add a check on the partial field. This is technically a breaking change in error semantics; we gave it a minor version because the old behavior was worse than the ambiguity, and we're prepared to argue about it.

Everything else

Upgrade notes

If you use…Do this
Table extractionNothing required. New structure is additive; ghost-row consumers should switch to row.spans.
Webhook consumersHandle partial: true (see above).
Python 3.8Upgrade Python. 3.8's been EOL since October 2024; we held on longer than we should have.
AsyncClientNothing. No, really — this release is API-compatible for async users.

Full changelog history lives here. Problems with the upgrade? support@unstructred.io — schema questions are our favorite kind, upgrade questions a close second.

#changelog#v2.3.0#tables
← benchmarks 2026 next: Postgres over Kafka →