Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

docx-scalpel

Anchor-addressed DOCX editing for LLM agents — a thin client over Docxodus' DocxSession.

docx-scalpel exposes Docxodus' stateful DOCX editor over a long-running .NET subprocess (docxodus-pyhost). The session lives in the host's memory until you explicitly release it, so an LLM agent can issue dozens of small edits against one document without paying the OOXML parse + Unid annotation + projection cost on every call.

Status: Alpha. linux-x64 wheels ship with a bundled docxodus-pyhost; other RIDs require a dev clone of Docxodus until the wheel matrix is extended (tracked in RELEASING.md).

Installation

pip install docx-scalpel

linux-x64 today; pre-release tags ship as 0.1.0a*, so use --pre if you want to opt in:

pip install --pre docx-scalpel

Source installs (pip install of the sdist, or pip install -e . from a dev clone) don't include a bundled host. Set DOCXODUS_HOST=/path/to/docxodus-pyhost to point at one you built, or run dotnet build tools/python-host/pyhost.csproj inside a Docxodus monorepo clone — the locator auto-discovers it.

Quick start

from docx_scalpel import open_session, FormatOp, Position

with open("contract.docx", "rb") as f:
    docx_bytes = f.read()

with open_session(docx_bytes) as session:
    # Walk template placeholders and fill them. The picker returns a string to
    # replace, or None to skip. fill_placeholders handles reverse-offset
    # ordering, $-prefix preservation, and multi-pass nested-bracket convergence
    # in one call.
    result = session.fill_placeholders(lambda p: "filled value")
    print(f"filled {result.filled} placeholders in {result.passes} passes")

    # Add a heading after the first body paragraph.
    proj = session.project()
    first_p = next(
        t for t in proj.anchor_index.values()
        if t.kind in ("p", "h") and t.scope == "body"
    )
    session.insert_paragraph(first_p.id, Position.AFTER, "## Reviewed by counsel")

    # Bold the first 8 characters of that paragraph.
    session.apply_format_by_substring(first_p.id, "Reviewed", FormatOp(bold=True))

    new_bytes = session.save()

with open("filled.docx", "wb") as f:
    f.write(new_bytes)

The with block is the documented lifecycle path — it calls session.close() on the way out, which releases the session from the host's SessionRegistry. A __del__ finalizer is a fallback for forgotten sessions but should not be relied on; interpreter shutdown may skip it.

Why a subprocess?

DocxSession holds a parsed WordprocessingDocument, an AnchorIndex of Unid-stamped block-level targets, a cached MarkdownProjection, and a bounded UndoRing of per-part XDocument snapshots. Recreating it costs tens of ms on small docs and seconds on large ones. The subprocess model lets one Python process drive many sessions across many calls, all in one host's memory, until you decide to close them.

Architecture:

Python process                 docxodus-pyhost (.NET 10)
─────────────                  ──────────────────────────
DocxSession  ──NDJSON──>       Dispatcher
                               │
                               ▼
                               DocxSessionOps
                               │
                               ▼
                               SessionRegistry (handle → DocxSession)

One host per Python process. Many sessions inside the host. atexit sends shutdown and (if the host doesn't comply) terminates / kills.

Full design + wire-protocol spec: docs/architecture/python_docxodus.md. Delta-spec for the docx-scalpel rebrand: docs/superpowers/specs/2026-05-26-docx-scalpel-design.md.

Development

Build the host binary (one-time)

# From the Docxodus repo root:
dotnet build tools/python-host/pyhost.csproj -c Release

This produces tools/python-host/bin/Release/net10.0/docxodus-pyhost. _host_locator.py discovers it automatically when you pip install -e . from a monorepo clone.

For non-monorepo development, set DOCXODUS_HOST=/path/to/docxodus-pyhost to override the discovery path.

Editable install + tests

cd python
python -m venv .venv
.venv/bin/pip install -e .[test]
.venv/bin/pytest -v

Test layout

  • tests/test_smoke.py — end-to-end mirror of Docxodus.Tests/DocxSessionSmokeTest.cs. v1 acceptance gate.
  • tests/test_lifecycle.py — proves session persistence, idempotent close, singleton host, finalizer fallback.

Tests share the Docxodus monorepo's TestFiles/ corpus so divergence between Python and .NET on identical inputs is detectable.

API surface

The DocxSession class exposes every op in Docxodus.Internal.DocxSessionOps as a snake-case method:

Tier Methods
Lifecycle save, close, undo, redo
Projection project, project_anchor
Discovery grep, grep_cross_block, find_placeholders, find_by_text, find_all_by_text, find_by_regex, find_by_kind, find_by_annotation, find_by_label, find_by_bookmark, list_annotations, exists, get_anchor_info, get_anchor_infos, get_edit_summary, remaining_placeholders, get_diff
A: text mutations replace_text, replace_text_range, replace_text_at_span, replace_inner, replace_match, delete_block, delete_range, delete_section
B: structural insert_paragraph, split_paragraph, merge_paragraphs
C: formatting apply_format, apply_format_by_substring, set_paragraph_style, set_list_level, remove_list_membership
D: tables replace_cell_content
Raw XML session.raw.get_xml, session.raw.insert_xml, session.raw.replace_xml

Every mutation method returns an EditResult envelope — transport-level failures raise DocxodusTransportError, but a business outcome (anchor_not_found, malformed_markdown, etc.) returns EditResult(success=False, error=EditError(...)). Never an exception across the API boundary.

Stateless functions

Alongside the session API, the package exposes stateless one-shot functions at the module root — no session handle, they take DOCX bytes in and return bytes / data out:

Function Signature Returns
convert_docx_to_html (data, options=None) HTML str
docx_diff_compare (left, right, settings=None) redlined DOCX bytes (native w:ins/w:del/w:moveFrom/w:moveTo/w:rPrChange markup)
docx_diff_get_revisions (left, right, settings=None) tuple[DocxDiffRevision, ...]
docx_diff_get_edit_script (left, right, settings=None) edit-script JSON str
docx_diff_accept_revisions (redline) bytes — accept every tracked change (≡ the right side of the diff)
docx_diff_reject_revisions (redline) bytes — reject every tracked change (≡ the left side)
docx_diff_consolidate (base, reviewers, settings=None) multi-author redlined DOCX bytes — merge N DocxDiffReviewer diffs against one shared base
docx_diff_get_conflicts (base, reviewers, settings=None) tuple[DocxDiffConflict, ...]
docx_diff_get_consolidated_revisions (base, reviewers, settings=None) tuple[DocxDiffConsolidatedRevision, ...]
docx_diff_get_consolidated_edit_script (base, reviewers, settings=None) edit-script JSON str

The docx_diff_* family is a thin client over Docxodus' DocxDiff IR diff engine. Tune pairwise comparisons with DocxDiffSettings and N-way consolidation with DocxDiffConsolidateSettings (whose conflict_resolution takes a ConflictResolution value). DetectMoves/format-change tracking, header/footer comparison, and per-reviewer attribution all round-trip through these calls.

from docx_scalpel import docx_diff_compare, docx_diff_get_revisions, DocxDiffSettings

with open("v1.docx", "rb") as f: left = f.read()
with open("v2.docx", "rb") as f: right = f.read()

redline = docx_diff_compare(left, right, DocxDiffSettings(author_for_revisions="Reviewer"))
for rev in docx_diff_get_revisions(left, right):
    print(rev.type, rev.text)

License

MIT. Built on top of Docxodus, which is itself a fork of Open-Xml-PowerTools.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

docx_scalpel-0.1.0a7.tar.gz (42.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

docx_scalpel-0.1.0a7-py3-none-win_amd64.whl (52.2 MB view details)

Uploaded Python 3Windows x86-64

docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_x86_64.whl (52.3 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_aarch64.whl (48.5 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

docx_scalpel-0.1.0a7-py3-none-macosx_11_0_arm64.whl (51.8 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file docx_scalpel-0.1.0a7.tar.gz.

File metadata

  • Download URL: docx_scalpel-0.1.0a7.tar.gz
  • Upload date:
  • Size: 42.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for docx_scalpel-0.1.0a7.tar.gz
Algorithm Hash digest
SHA256 d2bff13fc013a424018007ed1a488b272da98e04d32831b109dc75f9b0c84707
MD5 5756d681b15b2af69b9338d5466d1af0
BLAKE2b-256 7c1646e29c23c173e06b4794d04dcdb1dec4de92ad46ab311fb77dd3aa1c6a1c

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a7.tar.gz:

Publisher: python-publish.yml on JSv4/Docxodus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docx_scalpel-0.1.0a7-py3-none-win_amd64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a7-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 f3835b0a62d2f3cc5683fcf340dcf06aad6668e5d3a24c8730bc19c4a22170f1
MD5 d60a50395526683152f3d0cc4e317a43
BLAKE2b-256 e3b32cfc1e023959b5e481d6760aaeffb659868348829dd86f1e7e7d394af240

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a7-py3-none-win_amd64.whl:

Publisher: python-publish.yml on JSv4/Docxodus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 1b53c4d6908003f93e4486ba146c66420715d2b23aafebadf75c83f19bfcfd14
MD5 42ec5adfbe28b887d87f21eba7320ae6
BLAKE2b-256 faef8d5400ad2fa0f7d37a6590b008fa9232db5eeca9a4f7375851940b0a70bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_x86_64.whl:

Publisher: python-publish.yml on JSv4/Docxodus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 b5165fbf4c2b4a3997d924db22ee90b93b9722951dbd2b8b3eea02b9c5b5f533
MD5 fe24e6d41af7659bcecebaa5b40640fb
BLAKE2b-256 8671c44f4e4602305c323ae902ac8c3118029991325beb9213b2e4a9f21e80ca

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a7-py3-none-manylinux_2_28_aarch64.whl:

Publisher: python-publish.yml on JSv4/Docxodus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docx_scalpel-0.1.0a7-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a7-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 556523e3f6b6218c55dadcd249da1b8055eb433de917c4e053bd0706223d13f2
MD5 0827c9436f5a61b4c0486b6fe9a2efca
BLAKE2b-256 ad26b9d673e5b7fb7338f4d88e38cef4281c19f0c9c02129aa255f6e48902626

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a7-py3-none-macosx_11_0_arm64.whl:

Publisher: python-publish.yml on JSv4/Docxodus

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page