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.0a6.tar.gz (41.9 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.0a6-py3-none-win_amd64.whl (52.1 MB view details)

Uploaded Python 3Windows x86-64

docx_scalpel-0.1.0a6-py3-none-manylinux_2_28_x86_64.whl (52.2 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

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

Uploaded Python 3manylinux: glibc 2.28+ ARM64

docx_scalpel-0.1.0a6-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.0a6.tar.gz.

File metadata

  • Download URL: docx_scalpel-0.1.0a6.tar.gz
  • Upload date:
  • Size: 41.9 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.0a6.tar.gz
Algorithm Hash digest
SHA256 4e119c53a39c8caf86535df2f7af2c7d5209abc611434548cbec26c5b7b81914
MD5 2e1d8ddb5b67eebd14137a0c11c249a3
BLAKE2b-256 0f7937bbcaeba62c06610b8214f5213a190e73cd977dedbb914794e23a5987d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a6.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.0a6-py3-none-win_amd64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a6-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 de792340f412db10fbaaf2ebae977b4c1ffe3ea345f85bba39904a1bcd336728
MD5 0fdb048f5a1c2944126e3d0733095c40
BLAKE2b-256 028067ad60782654e51dc96c6b19df87fcdc2b983068cdadbaea4288c05d9e25

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a6-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.0a6-py3-none-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a6-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 8fa91ab4ce7b9f164d03f404e733562720898d9c3b14c3c120f3f287a7ba1104
MD5 2772106e93020bea7c6a0f8ab8ad327e
BLAKE2b-256 bc6fbd4d320a3d8b5b2741862401512baa935732206834ae858d6e4d0c63b8cf

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a6-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.0a6-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a6-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 fc07bd791b808a61e745e3f80b0458e979f07bb45b689e73e35dce9c99dd0361
MD5 789b6b51a6f65d4bf4bd91603a35e041
BLAKE2b-256 1e9c5f27efbbc0e51c5c152eb2c9928604ae3f195c259876ecdc8516725ae2cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a6-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.0a6-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for docx_scalpel-0.1.0a6-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e9a9f34b94f725c386f287e625a153d4c533d128f0269b2bc5e48cf2b86a2677
MD5 e906b8458926621c885f4ca7541fe007
BLAKE2b-256 0365321de39fc4d32161c38b1ab1b87d5b4b8290f0f0249c84c78e2d141b557a

See more details on using hashes here.

Provenance

The following attestation bundles were made for docx_scalpel-0.1.0a6-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