docx_plus
OOXML-level extensions for python-docx.
Documentation · API index · Architecture · Changelog · Roadmap
python-docx is an excellent library that stops at a well-defined boundary.
Past that boundary — the style cascade, content controls, anchored
comments, tracked changes, custom numbering, table borders — the usual
answer is a StackOverflow snippet that reaches into element._p and
builds raw lxml by hand. Everyone writing serious document automation
ends up with a private, half-tested pile of that code.
docx_plus is that pile, done properly: typed, tested against documents
Word itself authored, and schema-strict about where elements are allowed
to go. It composes with python-docx rather than replacing it — you
keep your Document object and reach for docx_plus only where you
need to.
from docx import Document
from docx_plus.styles import resolve_effective_formatting
doc = Document("report.docx")
# "Why is this heading 13pt and blue?" — a question python-docx can't answer,
# because the value is inherited, not set on the paragraph at all.
resolved = resolve_effective_formatting(doc.paragraphs[0], include_provenance=True)
print(resolved.font_size) # 13.0
print(resolved.provenance["font_size"]) # FormattingSource(layer='paragraphStyle',
# style_id='Heading2',
# chain_depth=0, ...)
Install
pip install docx-plus
uv add docx-plus
Requires Python 3.10+. The only dependencies are python-docx and lxml.
What it does
| Capability | Module | |
|---|---|---|
| Styles | Resolve the effective formatting of any paragraph / run / cell through the full eight-layer cascade, with per-field provenance. Create, modify, and remap styles; materialise any of 107 latent Word built-ins. | styles/ |
| Content controls | Text, dropdown, date, and checkbox controls via FormBuilder; read and write their values; round-trip through save / reopen. |
controls/ |
| Comments | Anchored comments with the body-side range markers python-docx omits — so Word's "show in document" actually works. Plus threading (reply / resolve / reopen), durable ids, and author presence. | comments/ |
| Tracked changes | Mark runs as insertions or deletions, read every revision with author / timestamp / text, accept or reject them, toggle track-changes mode. | revisions/ |
| Fields | PAGE / NUMPAGES / DATE and generic complex fields; mark fields dirty so Word recalculates on open. |
fields/ |
| Tables | Table / row / cell borders and shading, cell merging and unmerging, w:hMerge normalization, direct-formatting reads. |
tables/ |
| Numbering | Custom bullet and multi-level numbered list definitions, applied and restarted per paragraph. | numbering/ |
| Layout | Multi-column sections, mid-document section breaks, distinct even/odd headers, line numbering, page borders. | layout/ |
| Bookmarks | Paired body markers plus REF / PAGEREF cross-references. |
bookmarks/ |
| Notes | Footnotes and endnotes over the separate footnotes.xml / endnotes.xml parts; insert and edit in place. |
notes/ |
| Publishing | Table of Contents, figure / table captions via SEQ, Table of Figures. |
publishing/ |
| Protection | Form-fill, read-only, comments-only, or tracked-changes enforcement at the document level. | protection/ |
| Lint | Audit a document for direct formatting fighting the styles, skipped outline levels, hand-typed lists, and whitespace used as layout — then describe the repair as an ordered, serializable plan. Read-only. | lint/ |
| CLI | docx-plus inspect / restyle / controls / comments / lint / plan / skill — the library from a shell. |
cli/ |
Quickstart
A few of the most-used surfaces. The
documentation covers all of
them, and every module has runnable examples under
docx_plus/examples/.
Styles: define once, apply everywhere
from docx import Document
from docx_plus.styles import apply_style, create_style, ensure_style
doc = Document()
create_style(
doc, "BrandHeading",
style_type="paragraph",
based_on="Heading1",
font_name="Inter",
font_size=18.0,
color_rgb="2F5496",
bold=True,
spacing_after=240,
)
apply_style(doc.add_paragraph("Hello, world"), "BrandHeading")
doc.save("out.docx")
This is the Word-native workflow: define a style, apply it. Change the style later and every paragraph using it follows — unlike direct formatting, which you have to remember to update everywhere.
Word's built-ins (Heading1–Heading9, Title, Quote, TOC1–TOC9,
FootnoteText, …) are latent: defined by Word's defaults but absent
from styles.xml until used. ensure_style materialises them
idempotently, with defaults extracted from real Word-saved samples
rather than guessed:
ensure_style(doc, "Heading1") # materialises if absent
ensure_style(doc, "Heading1") # ...no-op the second time
ensure_style(doc, "TOC2") # 107 built-ins known
For documents authored elsewhere, where a style may be named "Heading 1"
with a space, ensure_style(doc, "Heading1", match_existing=True) finds
the existing definition via case- and space-insensitive matching — or use
remap_styles
for document-wide normalisation.
Forms: build a fillable document
from docx_plus.controls import FormBuilder
fb = FormBuilder() # or FormBuilder("template.docx")
fb.doc.add_heading("New employee form", level=1)
p = fb.doc.add_paragraph("Full name: ")
fb.add_text_control(p, tag="full_name", placeholder="Type your name")
p = fb.doc.add_paragraph("Department: ")
fb.add_dropdown(p, tag="dept", items=["Engineering", "Design", "Ops"])
p = fb.doc.add_paragraph("Start date: ")
fb.add_date_picker(p, tag="start_date", date_format="M/d/yyyy")
fb.save("form.docx")
Read and update an existing form's values:
from docx import Document
from docx_plus.controls import read_controls, set_control_value
doc = Document("form.docx")
set_control_value(doc, "full_name", "Ada Lovelace")
doc.save("filled.docx")
values = read_controls(Document("filled.docx"))
print(values["full_name"].value) # 'Ada Lovelace'
Comments: anchored to the text they're about
from docx import Document
from docx_plus.comments import add_comment, read_comments, reply_to_comment
doc = Document()
p = doc.add_paragraph()
p.add_run("Project Apollo ")
target = p.add_run("ships next quarter")
c = add_comment(target, "Optimistic — let's see what QA says.", author="Alice")
reply_to_comment(doc, c.comment_id, "Agreed, moving to Q3.", author="Bob")
for comment in read_comments(doc):
print(f"{comment.author}: {comment.text!r} on {comment.anchored_text!r}")
add_comment accepts a Run, a Paragraph, or a (start_run, end_run)
tuple for ranges. Unlike python-docx's Comments.add_comment — which
writes only the part-side body — docx_plus writes the three body-side
anchors, so the comment is attached to a real span of text.
Tracked changes: propose edits, then accept or reject
from docx import Document
from docx_plus.revisions import accept_revision, mark_insertion, read_revisions
doc = Document()
p = doc.add_paragraph("The report is ")
mark_insertion(p.add_run("nearly "), author="Alice")
p.add_run("complete.")
for rev in read_revisions(doc):
print(rev.revision_type, rev.author, rev.text) # 'insertion' 'Alice' 'nearly '
accept_revision(doc, rev.revision_id) # or reject_revision / accept_all_revisions
Publishing: TOC, captions, Table of Figures
from docx import Document
from docx_plus.fields import mark_fields_dirty
from docx_plus.publishing import add_caption, add_table_of_figures, add_toc
doc = Document()
doc.add_heading("Contents", level=1)
add_toc(doc.add_paragraph(), levels=(1, 2))
doc.add_heading("Architecture", level=1)
cap = doc.add_paragraph()
add_caption(cap, "Figure ", caption_type="Figure")
cap.add_run(": System overview.")
doc.add_heading("List of Figures", level=1)
add_table_of_figures(doc.add_paragraph(), caption_type="Figure")
mark_fields_dirty(doc) # Word populates TOC / SEQ / ToF on open
doc.save("paper.docx")
Command line
docx-plus installs a console command (also python -m docx_plus.cli)
for inspecting and editing documents from a shell:
$ docx-plus inspect report.docx --provenance # effective formatting per paragraph
$ docx-plus restyle draft.docx --target Heading1 -o clean.docx
$ docx-plus controls list form.docx --json # every content control
$ docx-plus controls set form.docx --tag name --value "Ada Lovelace" -o filled.docx
$ docx-plus comments list draft.docx --unresolved # open comment threads
$ docx-plus lint report.docx # formatting defects
$ docx-plus plan report.docx # what repairing them would change
$ docx-plus skill install # drop the agent skill into .claude/skills/
Read commands take --json. Mutating commands require -o/--output (or
an explicit --in-place) so the source is never overwritten by accident.
lint and plan exit 1 when they found something, so either drops
into a CI step directly. Full reference:
CLI docs.
For AI coding agents
docx_plus ships an agent skill inside the package — a structured
guide to the API that Claude Code (or any agent that reads skill files)
can load instead of guessing at signatures. pip install docx-plus is
enough to get it:
$ docx-plus skill install # copies it into ./.claude/skills/
See docx_plus/skill/SKILL.md and the
skills overview.
Documentation
Full docs are published at https://thomas-villani.github.io/docx-plus/, built with MkDocs and mkdocstrings.
- API index — hand-curated index of every public symbol, linked to the generated reference.
- Architecture — module layout, the cascade algorithm, schema-strict insertion, the error hierarchy, and the invariants the library maintains. Read this if you want to know why the OOXML looks the way it does.
- CLI reference.
- Test gaps — an honest accounting of where the suite has real holes.
Project status
v0.6.0, released 2026-07-31 — beta, and shipping. 2,043 tests,
96% coverage, mypy --strict clean with zero ignores. CI runs Python
3.10–3.13 on Linux plus a Windows job, and a lower-bound dependency job
pinned to python-docx==1.0.0 / lxml==4.9.0.
The API is stable in practice but pre-1.0: breaking changes are possible
on minor versions and will be called out in
CHANGELOG.md.
ROADMAP.md is the live record of what is shipped,
backlogged, and deliberately declined. Currently on the backlog:
content-control data binding to Custom XML Parts, bibliography and
BIBLIOGRAPHY fields, theme writing, glossary placeholder text, and
password-protected forms. If your use case needs
one of these, open an issue —
demand reorders the list.
Release history
- v0.1.0 — foundation (
core/), style inspection / modification / remapping, content controls, fields, and document protection. - v0.2.0 — comments, layout, bookmarks, notes,
core/parts, plus toggle properties, in-place edit verbs, line numbering, page borders, conditional table styles, andpublishing/. - v0.3.0 — tracked changes (
revisions/) and thedocx-plusCLI. - v0.4.0 — threaded comments over
commentsExtended.xml, anddocx-plus comments. - v0.5.0 — table formatting (
tables/), custom numbering (numbering/), comment durable ids and author presence (commentsIds.xml/people.xml), and the agent skill shipping in the wheel behinddocx-plus skill. - v0.6.0 — the document linter (
lint/) with 20 rules, profiles, andplan_fixes;docx-plus lint/plan; the cascade resolver corrected against live Word (toggles, conditional table formatting, theme colours, paragraph spacing, the default paragraph style); the document-wide sweep,stop_belowbaselines, andread_fields.
Contributing
Contributions are welcome — see
CONTRIBUTING.md for the development setup, the
quality gates, and the conventions. In short:
git clone https://github.com/thomas-villani/docx-plus.git
cd docx-plus
uv sync --extra dev
uv run pre-commit install
uv run pytest
Bug reports are most useful with a minimal .docx attached, or the
offending fragment of word/document.xml.
Security issues should be reported privately — see
SECURITY.md.
License
MIT. Copyright (c) 2026 Tom Villani, PhD. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file docx_plus-0.6.0.tar.gz.
File metadata
- Download URL: docx_plus-0.6.0.tar.gz
- Upload date:
- Size: 512.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89485b2618e99a80da5041813a4273d0394ede28a7e9fab514c194817196b2a5
|
|
| MD5 |
11811b97dc3f9fdc379449592d8320a9
|
|
| BLAKE2b-256 |
8577cc18dd58b4393d910cfaf440eb2e74a553872e244b75fb5b0d1622c189e0
|
Provenance
The following attestation bundles were made for docx_plus-0.6.0.tar.gz:
Publisher:
release.yml on thomas-villani/docx-plus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
docx_plus-0.6.0.tar.gz -
Subject digest:
89485b2618e99a80da5041813a4273d0394ede28a7e9fab514c194817196b2a5 - Sigstore transparency entry: 2309938591
- Sigstore integration time:
-
Permalink:
thomas-villani/docx-plus@17d670435ace265d8c8904d8d905cd20437bcf38 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/thomas-villani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@17d670435ace265d8c8904d8d905cd20437bcf38 -
Trigger Event:
push
-
Statement type:
File details
Details for the file docx_plus-0.6.0-py3-none-any.whl.
File metadata
- Download URL: docx_plus-0.6.0-py3-none-any.whl
- Upload date:
- Size: 346.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efd5a38dab10a53c41aae3d84abb1659ed2e5204ac78396dc21dd58d97c9424d
|
|
| MD5 |
faa05135abb860d33963675a026970ba
|
|
| BLAKE2b-256 |
6fe71890784ecf18f6575944f9fd0f4718ec6bd879996ced20428d56d1850331
|
Provenance
The following attestation bundles were made for docx_plus-0.6.0-py3-none-any.whl:
Publisher:
release.yml on thomas-villani/docx-plus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
docx_plus-0.6.0-py3-none-any.whl -
Subject digest:
efd5a38dab10a53c41aae3d84abb1659ed2e5204ac78396dc21dd58d97c9424d - Sigstore transparency entry: 2309938598
- Sigstore integration time:
-
Permalink:
thomas-villani/docx-plus@17d670435ace265d8c8904d8d905cd20437bcf38 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/thomas-villani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@17d670435ace265d8c8904d8d905cd20437bcf38 -
Trigger Event:
push
-
Statement type: