OOXML-level extensions for python-docx: style cascade, content controls, fields.
Project description
docx_plus
OOXML-level extensions for python-docx.
Composes with python-docx rather than replacing it: callers keep their
Document object and use docx_plus for the operations python-docx
can't reach.
v0.1 capabilities:
- Style cascade: read the effective formatting that would apply to any paragraph/run/cell, with per-field provenance; modify styles in the Word-native way rather than scattering direct formatting.
- Content controls: build text / dropdown / date / checkbox
controls with
FormBuilder; read their values back; round-trip them through save/reopen. - Fields: insert PAGE / NUMPAGES / DATE / generic complex fields; mark fields dirty so Word recalculates them on next open.
- Protection: enforce form-fill, read-only, comments-only, or tracked-changes mode at the document level.
Status: v0.1 complete. Pre-publication — not yet on PyPI. Read
SPEC.mdfor the API contract andIMPLEMENTATION.mdfor the build plan.
Install (development)
git clone https://github.com/thomas-villani/docx-plus.git
cd docx-plus
uv sync --extra dev # or: pip install -e ".[dev]"
60-second quickstart
Inspect: why does this paragraph look the way it does?
from docx import Document
from docx_plus.styles import resolve_effective_formatting
doc = Document("report.docx")
p = doc.paragraphs[0]
resolved = resolve_effective_formatting(p, include_provenance=True)
print(resolved.style_name) # e.g. "Title"
print(resolved.font_size) # e.g. 28.0 (points)
print(resolved.bold) # True / False / None
print(resolved.provenance["font_size"]) # FormattingSource(layer='paragraphStyle', ...)
ResolvedFormatting carries every formatting field that the OOXML
cascade can set — font_name, font_size, bold, italic, color_rgb,
alignment, indent_*, spacing_*, line_spacing, plus run-level
toggles. With include_provenance=True, every populated field is
keyed in .provenance to the cascade layer (and style ID) that
contributed it. That's how you answer "why is this paragraph 14pt
italic?" — the provenance tells you exactly which style in the
basedOn chain set the size and whether the italic came through XOR.
Modify: define a custom heading and apply it
from docx import Document
from docx_plus.styles import create_style, apply_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,
)
p = doc.add_paragraph("Hello, world")
apply_style(p, "BrandHeading")
doc.save("out.docx")
This is the Word-native workflow: define a style, apply it. Changing the style later changes every paragraph that uses it, not just the ones you remember to update.
Ensure: materialise a built-in latent style
Word's built-ins (Heading1–Heading9, Title, Quote, TOC1–TOC9,
FootnoteText, BlockText, PlainText, …) are latent — defined by
Word's defaults but not actually present in styles.xml until they're
used. ensure_style knows about 107 of them, with defaults
extracted from real Word-saved samples (not guessed):
from docx import Document
from docx_plus.styles import ensure_style, apply_style
doc = Document()
ensure_style(doc, "Heading1") # idempotent — materialises if absent
ensure_style(doc, "Heading1") # ...no-op the second time
ensure_style(doc, "TOC2") # also works for less-common built-ins
ensure_style(doc, "BlockText")
apply_style(doc.add_paragraph("Intro"), "Heading1")
The full list is tiered in Architecture §5 — Core/A–G cover essentially every style a Word user reaches for.
For documents authored elsewhere where IDs may not match (e.g. style
named "Heading 1" with a space), ensure_style(doc, "Heading1", match_existing=True) will find the existing definition via case- and
space-insensitive matching, or use remap_styles
for document-wide normalisation.
Forms: build a fillable document with FormBuilder
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")
p = fb.doc.add_paragraph("Remote? ")
fb.add_checkbox(p, tag="remote", checked=False)
fb.save("form.docx")
Read or update an existing form's values with read_controls /
set_control_value:
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")
set_control_value(doc, "dept", "Engineering")
doc.save("form_filled.docx")
values = read_controls(Document("form_filled.docx"))
print(values["full_name"].value) # 'Ada Lovelace'
print(values["dept"].value) # 'Engineering'
Fields and protection: page numbers + lock-down
from docx import Document
from docx_plus.fields import add_page_number_field, mark_fields_dirty
from docx_plus.protection import protect_document
doc = Document()
p = doc.add_paragraph("Page ")
add_page_number_field(p)
p.add_run(" of ")
add_page_number_field(p, field="NUMPAGES")
mark_fields_dirty(doc) # Word recalculates fields on open
protect_document(doc, mode="forms") # only content controls editable
doc.save("report.docx")
add_date_field and the generic add_field(instruction=..., initial_text=...)
cover dates and any other complex field (TOC, REF, MERGEFIELD, …).
unprotect_document(doc) removes any protection;
is_protected(doc) is a one-liner predicate.
What's next
v0.1 ships the four capabilities listed at the top of this README.
The v0.2 deferred list
(SPEC §15) tracks what comes after — anchored comments, footnotes /
endnotes, bookmarks and cross-references, a sections/ API for
columns and mid-document section breaks, content-control data binding
to Custom XML Parts, theme writing, and password-protected forms.
Open an issue if your use case needs any of these and you'd like to
help shape the design.
Build phases (for contributors)
| Phase | Deliverable | Status |
|---|---|---|
| 1 | Foundation (core/ns, core/oxml, core/ids, _testing/) |
✓ complete |
| 2 | Style inspection (styles/inspect, styles/theme) |
✓ complete |
| 3 | Style modification (styles/modify) |
✓ complete |
| 3.5 | Style remapping (find_matching_style, remap_styles, ensure_style(match_existing=)) |
✓ complete |
| 4 | Content controls (controls/) |
✓ complete |
| 5 | Fields + document protection (fields/, protection/) |
✓ complete |
| 6 | Polish — examples, headless LibreOffice smoke tests, CI doc build | ✓ complete |
Documentation
Full docs (rendered by MkDocs + mkdocstrings) are published at https://thomas-villani.github.io/docx-plus/.
- Architecture — module layout, cascade algorithm, schema-strict insertion, error hierarchy, invariants
- API Index — hand-curated index of every public symbol with links to the auto-generated reference
- Test Gaps — honest accounting of where the test suite has real holes (snapshot at end of Phase 5)
- Per-module API reference lives under
https://thomas-villani.github.io/docx-plus/reference/;
uv run mkdocs serveto browse locally.
License
MIT. Copyright (c) 2026 Tom Villani, PhD. See LICENSE.
Project details
Release history Release notifications | RSS feed
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.1.0.tar.gz.
File metadata
- Download URL: docx_plus-0.1.0.tar.gz
- Upload date:
- Size: 136.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fa2d382a30e8e4b1c2a59f6aadfb4550940e1e9751874b3622ae14539a19864
|
|
| MD5 |
a3ba8bf1ee0ebb715441120d77c700a2
|
|
| BLAKE2b-256 |
6ad32b18b0e9fda2f210a80e12b4b23dfb024df89c4a652079b67e615cc74fc3
|
Provenance
The following attestation bundles were made for docx_plus-0.1.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.1.0.tar.gz -
Subject digest:
5fa2d382a30e8e4b1c2a59f6aadfb4550940e1e9751874b3622ae14539a19864 - Sigstore transparency entry: 1575043559
- Sigstore integration time:
-
Permalink:
thomas-villani/docx-plus@ae2abbca8f72a98e21c77bbf94bb9037542e70ad -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/thomas-villani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ae2abbca8f72a98e21c77bbf94bb9037542e70ad -
Trigger Event:
push
-
Statement type:
File details
Details for the file docx_plus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: docx_plus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 65.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e2d125452b29e67a0b14392cdd05c274034d2ddea1517c2945f80fc95d1ef61
|
|
| MD5 |
6e9d5252130564c3777ba34ae31056f3
|
|
| BLAKE2b-256 |
2f212c21c4f509f62a8a55c1ef135f67118455b8ee38330a8298db461313215d
|
Provenance
The following attestation bundles were made for docx_plus-0.1.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.1.0-py3-none-any.whl -
Subject digest:
1e2d125452b29e67a0b14392cdd05c274034d2ddea1517c2945f80fc95d1ef61 - Sigstore transparency entry: 1575043583
- Sigstore integration time:
-
Permalink:
thomas-villani/docx-plus@ae2abbca8f72a98e21c77bbf94bb9037542e70ad -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/thomas-villani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ae2abbca8f72a98e21c77bbf94bb9037542e70ad -
Trigger Event:
push
-
Statement type: