Skip to main content

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: Beta. Wheels ship a bundled docxodus-pyhost for linux-x64, linux-arm64, osx-arm64, and win-x64; any other platform installs from the sdist and needs a host of its own (see below).

Installation

pip install docx-scalpel

That resolves a wheel on linux-x64, linux-arm64, osx-arm64, or win-x64 — each carrying a self-contained docxodus-pyhost built from the same commit as the release, so there's no .NET runtime to install and no version to pin.

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.

A dotnet build host is framework-dependent, so it needs the .NET 10 runtime at launch. If your system dotnet is older and .NET 10 lives elsewhere (e.g. ~/.dotnet), the host will exit with You must install or update .NET to run this application; export DOTNET_ROOT to point at the newer install. Released wheels are unaffected — they bundle a self-contained host with no runtime lookup.

export DOTNET_ROOT="$HOME/.dotnet"

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, to_html
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
Inspection get_block_metadata, get_block_metadatas, get_list_membership, get_section_info
A: text mutations replace_text, replace_text_range, replace_text_at_span, replace_inner, replace_match, delete_block, move_block, delete_range, delete_section
B: structural insert_paragraph, split_paragraph, merge_paragraphs
B: headers/footers/page numbers set_header_text, set_footer_text, ensure_header_footer_visible, insert_page_number_field, set_page_numbering, clear_page_numbering
B: footnotes/endnotes insert_footnote, insert_endnote
B: native comments add_comment, add_comment_to_revision, add_comment_reply, update_comment, set_comment_resolved, remove_comment, list_comments
C: formatting apply_format, apply_format_by_substring, set_paragraph_style, set_paragraph_format, set_list_level, remove_list_membership, apply_list_format, apply_list_format_range, set_list_start_override, clear_list_start_override
D: tables replace_cell_content
D: tracked changes set_tracked_changes, set_revision_author, list_revisions, accept_revision, reject_revision
E: annotations add_annotation, remove_annotation, update_annotation, move_annotation
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.0.tar.gz (58.1 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.0-py3-none-win_amd64.whl (53.2 MB view details)

Uploaded Python 3Windows x86-64

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

Uploaded Python 3manylinux: glibc 2.28+ x86-64

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

Uploaded Python 3manylinux: glibc 2.28+ ARM64

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

Uploaded Python 3macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: docx_scalpel-0.1.0.tar.gz
  • Upload date:
  • Size: 58.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for docx_scalpel-0.1.0.tar.gz
Algorithm Hash digest
SHA256 33d3f9e006e961e8b37e31eca6740b74790a942250ca0b66f1e1c5d398a24725
MD5 a8dac2a1cae66dd5fe6e2fa44d1ffce7
BLAKE2b-256 f8b628ec91fe6842bada8bc6c5dfb429839fd21f4efcea6ffa7f0d078d3877d9

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: docx_scalpel-0.1.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 53.2 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for docx_scalpel-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 656162c4d9ee382d368df9a37d9b6ecfa3392989a4425a207db84ff1826d1bc0
MD5 83eee943f6c4e15283675c04af5fa3d3
BLAKE2b-256 25dde4b8aa6a88b17d4bc4e9344867eabf7c48503e3c374206ce7150f6212f7a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for docx_scalpel-0.1.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 2c4514f7122501383ca260a2b4796d71466148f023fc2a7b6e63f1605677a7df
MD5 6bcc9441fcaa327a12f5dfa4d8f60369
BLAKE2b-256 3b04ca6b934366886c7209c910895cf495e38512de962f3055b221b09ba2e9a5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for docx_scalpel-0.1.0-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 751dc5eee5ac782cee736f65c1d23a9b61daf671344b7c32ce26fff2d2b2c815
MD5 19f5a270868f6f602da3d269a42dfbc8
BLAKE2b-256 0e89c33b59672fb66ebe59ebc510d61c959492861d5c5dc8f3036753369c1a46

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for docx_scalpel-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1c316735ccb6db97d9a14ef56332e9aa4c1d19e9e1db6942f8d1b172448500ff
MD5 4a1ea5672ff464c0e113f8b58c3fe289
BLAKE2b-256 13e77fde033e593c3c4d8dcaeba098415e81e78d9cb007f22215e0fe24a8aaf2

See more details on using hashes here.

Provenance

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