Skip to content

docx_plus — API Reference

Looking for how to do something?

This page is an exhaustive symbol index, not a tutorial. The guides are the task-oriented layer — one page per capability, with working code — and the concepts explain why the OOXML behaves as it does.

This file is the hand-curated index of every public symbol. The full reference (signatures, docstrings, source links) is built by MkDocs with the mkdocstrings Python handler. Per-module pages live under reference/ and are populated from the Google-style docstrings on each symbol — there is no separate "regenerate docs" step beyond running mkdocs.

Serving the docs site locally

uv run mkdocs serve
# or:
mkdocs serve

Opens at http://127.0.0.1:8000. Live-reloads on file change. The nav is configured in mkdocs.yml at the repo root.

Building a static site

uv run mkdocs build

Output lands in site/ (gitignored). CI wires this on every push to main (Phase 6).

To browse without serving — read source. Every public symbol has a Google-style docstring (enforced by ruff's D ruleset on docx_plus/).


Public surface at v0.6.x

v0.1's six phases, the initial v0.2 cycle (comments, layout, bookmarks / cross-references, footnotes / endnotes), the v0.2 in-place expansion (toggle props, in-place comment / note edits, line numbering, page borders, conditional table-style formatting, publishing module), the v0.3 cycle (tracked changes, the docx-plus CLI), the v0.4 cycle (threaded comments with resolve / reopen), and the v0.5 cycle (table formatting, custom numbering, comment durable ids and author presence, the packaged agent skill), and the v0.6 cycle (the lint layer — lint, plan_fixes, and the lint / plan CLI commands — plus the cascade corrections, the document sweep, stop_below baselines, and read_fields) are all complete. The v0.6.x patch releases extend the sweep to headers, footers, notes and comments (parts=), let styles link numbering (num_id / num_level), and route lint profile options through to the rules that read them. Fourteen runnable example scripts in docx_plus/examples/ demonstrate the surface: inspect_document.py, restyle_existing.py, build_form.py, populate_form.py, add_comments.py, threaded_comments.py, multi_column_layout.py, bookmarks_and_xrefs.py, footnotes_and_endnotes.py, publishing_layout.py, track_changes.py, table_formatting.py, custom_numbering.py, lint_document.py. Start there if you want to see the library in motion before reading the index.

docx_plus (top-level package)

Symbol Kind Notes
DocxPlusError exception Root of every typed library error. See the error hierarchy
__version__ str "0.6.2"

docx_plus.core

The foundation primitives. Every capability module imports from here only.

Symbol Kind Notes
DocxPlusError exception Re-export of the top-level root
IdRegistry(doc) class Per-document SDT w:id allocator. See core/ids.py
IdRegistry.next() method Issue a fresh 31-bit positive w:id, chosen at random
IdRegistry.next_sequential() method v0.5. Issue the lowest unused id instead — Word's convention for numbering, where a file full of nine-digit ids would be needlessly unreadable. Gap-filling
IdRegistry.reserve(value) method Reserve a specific value or raise DuplicateIdError
IdRegistry.issued() method Frozenset snapshot of all issued IDs
ParaIdRegistry(doc) class v0.4. Per-package w14:paraId allocator — threaded comments key their parent/child links off it, so it seeds from the body plus the comments / footnotes / endnotes parts. next_hex() renders the 8-uppercase-hex-digit form
DuplicateIdError exception Dual-bases: DocxPlusError, ValueError. reserve() on an already-issued value
IdRangeError exception Dual-bases: DocxPlusError, ValueError. A reserved id falls outside the 31-bit positive range
RegistryExhaustedError exception v0.6.2. Dual-bases: DocxPlusError, RuntimeError. A registry has no value left to issue. Effectively unreachable (the w:id space is 31 bits, the _Ref name space nine digits) but typed so the one path in core/ that could abort a command is still a DocxPlusError
qn(name) function "w:tag" → Clark-notation {namespace}tag
InvalidNamespaceError exception Dual-bases: DocxPlusError, ValueError. qn() got a malformed name or unknown prefix
NSMAP dict The library's pre-bound query namespace map (w, w14, w15, w16cid, r, mc, a, xml)
BUILD_NSMAP dict v0.6.2 (re-exported). The narrower build map (w, w14, r, mc, a, xml) — what el() binds on a new element that is going into document.xml, so the element does not carry the w15 / w16cid declarations the query map needs
W, W14, R, MC str Namespace URIs — WordprocessingML main, the Word 2010 extension (w14:paraId), relationships, and markup compatibility
W15 str The Word 2012 extension namespace URI — commentsExtended.xml, people.xml. v0.4
W16CID str The Word 2016 extension namespace URI — commentsIds.xml. v0.5
XML str XML namespace URI (added Phase 5 to make qn("xml:space") work for w:instrText)
el(tag, **attrs) function Create a namespaced element
sub(parent, tag, **attrs) function Create + append a namespaced child
xpath(node, expr) function XPath against node with NSMAP pre-bound. Use this — BaseOxmlElement.xpath() rejects namespaces= kwarg
remove(node) function Detach from parent, no-op if already detached
on_off(value, *, absent=False) function v0.6.2. The reader for an ECMA-376 ST_OnOff attribute — 1 / 0, true / false, on / off. absent is what a missing attribute means (<w:b/> is on, so toggle callers pass True). Do not open-code the test: reading off as anything but false inverts the property
parse_ooxml_datetime(value) function v0.6.2. Parse an xsd:dateTime attribute, tolerating the trailing Z Word writes (which fromisoformat rejected before 3.11). Returns None for a missing, empty, or unparseable value rather than raising — a bad date on a comment is no reason to refuse the rest of it
body_document_for(proxy, *, operation=...) function Resolve the owning main-body Document from a python-docx proxy; raises ValueError for header/footer proxies. Shared by comments / notes
build_complex_field(p_element, instruction, initial_text) function Emit the 5-run complex-field sequence (begin / instrText / separate / result / end). Used by fields/simple.py and bookmarks/crossref.py
build_bookmark(start_anchor, end_anchor, *, bookmark_id, name) function v0.5. Bracket a range with a bookmarkStart / bookmarkEnd pair. Lives in core so publishing can make a caption referenceable — a REF field can only point at a bookmark, never at the caption's SEQ field
validate_bookmark_name(name, *, arg_name="name") function v0.5. Check Word's bookmark-name grammar. Shared by the three surfaces that accept one, because a name only Word's UI would reject yields a silently unresolved field
BookmarkIdRegistry(doc) class Bookmark w:id allocator. Moved here from bookmarks in v0.5 and re-exported there
BookmarkNameRegistry(doc) class v0.5. Bookmark name allocator — guards duplicates (which make a REF ambiguous) and mints hidden _Ref + 9-digit anchors via next_ref_name()
DuplicateBookmarkNameError exception Dual-bases: DocxPlusError, ValueError. BookmarkNameRegistry.reserve on a name already in use
insert_before_first_anchor(parent, new_element, anchor_tags) function Schema-strict insertion helper for settings.xml mutations. Used by fields/update.py and layout/settings.py
ordered_insert(parent, child, order) function v0.5. Idempotent schema-ordered insert given the parent's full child sequence — replaces any same-tag sibling. The stronger form of the above; promoted out of styles/modify.py so numbering can share it
Border dataclass (frozen) The CT_Border shape — style, size, color, space — shared by page, table, and cell borders. Defined in layout in v0.2, moved to core in v0.5; docx_plus.layout.Border still works
border_attrs(border) function v0.5. Serialize a Border to its four OOXML attributes
get_or_create_part(doc, spec) function Return (part, root_element) for a separate OOXML part (creates and wires the relationship if absent). v0.2
PartSpec dataclass (frozen) Identification data for get_or_create_part. Use the pre-baked constants below or build your own
COMMENTS_SPEC PartSpec /word/comments.xml
COMMENTS_EXTENDED_SPEC PartSpec /word/commentsExtended.xml — comment threading. v0.4
COMMENTS_IDS_SPEC PartSpec /word/commentsIds.xml — durable comment ids. v0.5
PEOPLE_SPEC PartSpec /word/people.xml — comment author presence. v0.5
NUMBERING_SPEC PartSpec /word/numbering.xml — list definitions. v0.5. Needed because DocumentPart.numbering_part fabricates through NumberingPart.new(), an unimplemented stub that raises NotImplementedError
FOOTNOTES_SPEC PartSpec /word/footnotes.xml
ENDNOTES_SPEC PartSpec /word/endnotes.xml
CT_COMMENTS_EXTENDED / RT_COMMENTS_EXTENDED str Content- and relationship-type URIs for the extended part. Microsoft extensions, absent from python-docx's CT / RT enums. v0.4
CT_COMMENTS_IDS / RT_COMMENTS_IDS, CT_PEOPLE / RT_PEOPLE str Same, for the two v0.5 comment side-parts

docx_plus.styles — inspection

The cascade resolver. See The cascade resolver for the algorithm walkthrough.

Symbol Kind Notes
resolve_effective_formatting(target, *, include_provenance=False, table_context=None, stop_below=None) function The headline API — walks the eight cascade layers, returns ResolvedFormatting. table_context overrides the auto-derived cell position for conditional table-style branches. stop_below (v0.6) resolves the target as if that layer and everything above it were absent — the baseline "what would this look like without its own direct formatting?" question the linter is built on. A paragraph whose w:pStyle is absent, dangling, or of the wrong w:type resolves through the default paragraph style, which is what style_id then reports
Layer type alias v0.6. The eight cascade layers in order: docDefaults, tableStyle, paragraphStyle, styleNumbering, numbering, directParagraph, runStyle, directRun. What stop_below accepts and FormattingSource.layer reports
StyleKind type alias Literal["paragraph", "character", "table", "numbering"] — the w:type of a w:style. A style reference only resolves to a style of the type it demands
StyleType type alias v0.6.2. Alias of StyleKind, exported from docx_plus.styles — the name the modification side (create_style(style_type=...), StyleInfo.style_type) has always used
AUTO_COLOR str v0.6. "auto" — what color_rgb holds for Word's Automatic. The one value of that field which is not RRGGBB hex, so color_rgb is not None does not imply a parseable hex value. Automatic is a colour Word applies, not the absence of one: it overrides whatever a lower layer set. modify_style accepts it, so a resolved value round-trips
iter_resolved_paragraphs(doc, *, include_provenance=False, include_runs=True, include_tables=True, include_baseline=False, include_spacing=False, parts=("body",)) function v0.6. The document-wide sweep — every paragraph and run resolved against one shared cache, in document order, yielding lazily. Identical results to calling resolve_effective_formatting per target, but the theme, styles part and every basedOn chain are read once. include_spacing=True (v0.6.2) also computes each paragraph's ParagraphSpacing against the shared cache. Body only by default; parts= (v0.6.2) takes any of the SweepPart names or "all", which adds every header, footer, footnote, endnote, and comment after the body
SweepPart type alias v0.6.2. Literal["body", "header", "footer", "footnotes", "endnotes", "comments"] — what parts= accepts and ResolvedParagraph.part reports
InvalidSweepPartError exception v0.6.2. Dual-bases: DocxPlusError, ValueError. parts= named something that is not a SweepPart
ResolvedParagraph dataclass (frozen) v0.6. paragraph, index, formatting, runs, table_depth, optional baseline, optional spacing (v0.6.2, with include_spacing=True), part (v0.6.2 — always "body" unless swept with parts=); in_table and text properties. A paragraph from a footnotes, endnotes, or comments part is wrapped in a Paragraph whose .part is that side part, so its .style accessor does not work; .text, .runs and the resolved formatting do. runs includes runs inside a <w:hyperlink>, which Paragraph.runs omits, so runs and text cover the same content
ResolvedRun dataclass (frozen) v0.6. run, index, formatting, optional baseline (the same run with stop_below="directRun")
resolve_paragraph_spacing(paragraph) function The vertical space Word actually leaves around a paragraph, after <w:contextualSpacing> and Word's space-after/space-before arithmetic. Returns ParagraphSpacing
ParagraphSpacing dataclass (frozen) space_above / space_below (the applied gaps, in twips), declared_before / declared_after, contextual_spacing, before_suppressed / after_suppressed. One paragraph's space_below equals the next one's space_above
ResolvedFormatting dataclass (frozen) 37 formatting fields + partial + optional provenance. SPEC §4. All twelve ECMA-376 17.7.3 toggles are surfaced (bold, italic, cs_bold, cs_italic, caps, small_caps, strike, vanish, emboss, imprint, outline, shadow). spacing_before / spacing_after are what the cascade declares; contextual_spacing carries the flag, and resolve_paragraph_spacing applies it
FormattingSource dataclass (frozen) layer, style_id, chain_depth, is_toggle_resolved. layer is one of docDefaults, tableStyle, paragraphStyle, styleNumbering, numbering, directParagraph, runStyle, directRun
TableContext dataclass (frozen) Cell position within a table — is_first_row, is_last_row, is_first_col, is_last_col, the four is_band* fields — plus the table's w:tblLook gating (first_row_enabled, last_row_enabled, first_col_enabled, last_col_enabled, all defaulting to True). Drives <w:tblStylePr> branch selection (firstRow, lastRow, band1Horz, …)
StyleCascadeError exception basedOn cycles or depth > 11
InvalidLayerError exception v0.6.2. Dual-bases: DocxPlusError, ValueError. stop_below named something that is not a Layer (was a bare ValueError)
MissingPartError exception Referenced part absent (reserved — currently no caller raises it)

docx_plus.styles — modification

Style creation, modification, application, removal, and reconciliation.

Symbol Kind Notes
create_style(doc, style_id, *, style_type, name, based_on, next_style, linked_style, ui_priority, q_format, custom, **properties) function Define a new style. Raises StyleExistsError if style_id is already defined
modify_style(doc, style_id, *, if_missing, **properties) function Mutate one or more properties. Merge semantics for w:ind/w:spacing/w:rFonts
apply_style(target, style_id) function Apply by ID to Paragraph | Run | _Cell. Raises StyleNotFoundError
delete_style(doc, style_id, *, force=False) function Remove. Raises StyleInUseError unless force=True (leaves dangling refs)
ensure_style(doc, style_id, *, match_existing=False, **defaults_if_creating) function Idempotent. Materialises latent built-ins from _BUILTIN_STYLES if absent
find_matching_style(doc, target_id) function Case/space-insensitive lookup against w:styleId and w:name. See Style remapping
remap_styles(doc, *, targets=None, mapping=None, create_missing=False) function Bulk reconciliation via four-step fallback. Rewrites body refs only
list_styles(doc, *, style_type=None, include_latent=False) function Enumerate. include_latent=True adds built-ins from _BUILTIN_STYLES
StyleProxy class Lightweight live wrapper around a w:style element
StyleProxy.modify(**properties) method Delegate to modify_style
StyleProxy.delete(*, force=False) method Delegate to delete_style
StyleInfo dataclass Returned by list_styles: style_id, name, style_type, based_on, is_default, is_latent, is_builtin (v0.6 — a style Word ships rather than one the author created)
find_unused_styles(doc) function v0.6. Author-created styles defined but referenced nowhere — by a paragraph, a run, a table, a basedOn chain, or a numbering definition. The public API behind the unused-styles lint rule. Built-ins are excluded: Word defines a hundred latent styles a document is not obliged to use
StyleExistsError exception create_style on duplicate ID
StyleNotFoundError exception Referenced ID not defined
StyleInUseError exception delete_style without force=True on referenced style
UnknownStylePropertyError exception Dual-bases: DocxPlusError, TypeError. Unknown **properties kwarg
InvalidColorError exception Dual-bases: DocxPlusError, ValueError. A color_rgb value that isn't valid RRGGBB hex
InvalidStylePropertyError exception v0.6.2. Dual-bases: DocxPlusError, ValueError. The right property name with the wrong value — a negative num_id, a num_level outside 0–8, or num_level on a style that links no numbering definition

Properties accepted by create_style / modify_style

Field names match ResolvedFormatting so output round-trips back through the modifier without translation. Paragraph-level: alignment, indent_left, indent_right, indent_first_line, spacing_before, spacing_after, line_spacing, line_spacing_rule, keep_with_next, keep_lines, page_break_before, outline_level, and (v0.6.2) num_id / num_level. Run-level: font_name, font_size, bold, italic, underline, strike, color_rgb, highlight, caps, small_caps, vanish, vert_align, and the complex-script / decorative toggles cs_bold, cs_italic, emboss, imprint, outline, shadow.

num_id / num_level write w:pPr/w:numPr, linking the style to a numbering definition. num_id is the w:numId that numbering.define_list_definition (or a preset) returned; 0 is the schema's "explicitly not numbered" sentinel (how a style based on a list style opts out) and None removes the link entirely. num_level is the 0-based w:ilvl (0–8); it defaults to 0 when only num_id is given and raises InvalidStylePropertyError on a style that links no definition.

docx_plus.styles.theme

Read-only theme color resolution. Theme writing is on the backlog — see ROADMAP.md.

Symbol Kind Notes
load_theme(doc) function Read word/theme/theme1.xml (a:clrScheme + a:fontScheme). Returns None on missing/malformed
ThemeColors(scheme, fonts={}, mapping={...}) dataclass (frozen) Holds the parsed a:clrScheme, a:fontScheme, and the document's <w:clrSchemeMapping>
ThemeColors.base(theme_name) method Lookup color by Word ST_ThemeColor name, resolved through mapping; returns None for unknowns
ThemeColors.font(token) method Lookup typeface by ST_Theme font token (minorHAnsi, …); returns None for unknowns
resolve_theme_color(theme, name, *, tint=None, shade=None) function Translate aliases + apply tint/shade. Returns hex RRGGBB
resolve_theme_font(theme, token) function Resolve a *Theme font token to its concrete typeface (e.g. minorHAnsiCalibri)
apply_theme_tint(hex_color, tint_byte) function Lighten toward white
apply_theme_shade(hex_color, shade_byte) function Darken toward black
apply_lum_mod(hex_color, lum_mod) function Multiply lightness by per-mille factor (ECMA-376 17.18.40)
apply_lum_off(hex_color, lum_off) function Add to lightness by per-mille factor
ThemeError exception Structurally invalid input to the transforms

docx_plus.controls — build side

Build content controls (SDTs) and attach them inline to paragraphs. Architecture walkthrough in Content controls.

Symbol Kind Notes
FormBuilder(document_or_path=None, *, id_registry=None) class Wrap a Document, open one from path, or start fresh. On construction: materialises PlaceholderText style, verifies w14 namespace, seeds IdRegistry
FormBuilder.doc attribute The underlying python-docx Document — use it for ordinary content (headings, paragraphs, tables)
FormBuilder.add_text_control(paragraph, *, tag, alias=None, placeholder=..., multiline=False) method Single- or multi-line text SDT. Returns the w:sdt element
FormBuilder.add_dropdown(paragraph, *, tag, items, alias=None, placeholder=..., editable=False) method Dropdown (or combobox if editable=True). items is list[str] or list[tuple[display, value]]
FormBuilder.add_date_picker(paragraph, *, tag, alias=None, placeholder=..., date_format="M/d/yyyy", lcid="en-US") method Date picker SDT
FormBuilder.add_checkbox(paragraph, *, tag, alias=None, checked=False) method Checkbox via w14:checkbox
FormBuilder.save(path) method Save the wrapped document. Returns the path as str
DropdownItem type alias str | tuple[str, str] — display-only or (display, value)
MissingNamespaceError exception Document root doesn't declare w14add_checkbox would emit unrenderable XML
InvalidDropdownItemError exception Dual-bases: DocxPlusError, TypeError. An items entry that isn't a str or (display, value) tuple

docx_plus.controls — read side

Symbol Kind Notes
list_controls(doc) function Returns list[ControlValue] — every control, document order, nothing keyed. Use on Word-authored documents
read_controls(doc, *, by="tag") function Returns dict[str, ControlValue] keyed by tag (default) or alias. Controls with no usable key are omitted
set_control_value(doc, tag, value, *, control_id=None) function Update one control. Type-dispatched on the control type. control_id disambiguates a repeated tag
clear_control(doc, tag, *, control_id=None) function Reset to the placeholder state
ControlValue dataclass (frozen) tag (str \| None), alias, control_type, value, is_placeholder, control_id, index, location
ControlValueT type alias v0.6.2 (exported). str \| bool \| datetime — the Python type of ControlValue.value and what set_control_value accepts
ControlType type alias The five writable types plus richtext, picture, group, repeating, repeatingitem, docpart, citation, bibliography, equation
WRITABLE_TYPES frozenset The ControlType values set_control_value / clear_control accept
ControlNotFoundError exception Dual-bases: DocxPlusError, KeyError. No control matched the tag or id
DuplicateTagError exception Dual-bases: DocxPlusError, ValueError. A tag doesn't identify exactly one control. Note w:tag is optional and non-unique in OOXML — Word writes w:val="" by default
ValueNotInListError exception Dual-bases: DocxPlusError, ValueError. Dropdown value matches neither w:value nor w:displayText. Combobox is exempt — it accepts freeform
ControlTypeError exception Dual-bases: DocxPlusError, TypeError. set_control_value value type doesn't match the control type

docx_plus.fields

Complex field insertion (PAGE / DATE / generic) and the "recalculate on open" flag. Architecture walkthrough in Fields and protection.

Symbol Kind Notes
add_page_number_field(paragraph, *, field="PAGE", format=None) function Append a PAGE / NUMPAGES / SECTIONPAGES field. format is a field-switch string like r"\* ARABIC". Returns the begin <w:r>
add_date_field(paragraph, *, format="MMMM d, yyyy", auto_update=True) function Append a DATE (auto-update) or CREATEDATE (frozen) field with a Word date-format string
add_style_reference(paragraph, *, style, search_from_bottom=False, number=None, position=False, suppress_non_delimiters=False, preserve_formatting=True) function v0.5. STYLEREF — the text of the nearest paragraph with a given style, re-resolved per page. The one cross-reference needing no bookmark. style is the style name ("Heading 1"), or an int outline level
StyleRefNumber type alias "plain" / "relative" / "full" — how much context a STYLEREF number carries (\n / \r / \w)
add_field(paragraph, *, instruction, initial_text="") function Generic complex field. Use for TOC, REF, MERGEFIELD, etc. Spaces are normalised around instruction
mark_fields_dirty(doc) function Set w:updateFields val="true" in settings.xml. Idempotent
PageFieldName type alias Literal["PAGE", "NUMPAGES", "SECTIONPAGES"]
read_fields(doc, *, keyword=None) function v0.6. Every complex field in the body, in document order, as FieldInfo; keyword filters by type, case-insensitively. Body only — a PAGE field in a header is not reported. See reference/fields-read.md
FieldInfo dataclass v0.6. keyword (upper-cased), arguments (remaining tokens, quotes stripped, switches in order), instruction (raw), result (the cached text Word last rendered — not authoritative), paragraph_index (0-based, counts table-cell w:ps like read_bookmarks), begin_element; switches and operands properties

docx_plus.protection

Document-level edit-mode enforcement. Unpassworded — v0.1 by design; password-protected forms are v0.2 (SPEC §1).

Symbol Kind Notes
protect_document(doc, *, mode="forms") function Emit w:documentProtection with w:edit=mode + w:enforcement="1". Idempotent — second call replaces mode
unprotect_document(doc) function Remove protection. Idempotent
is_protected(doc) function Presence predicate (does not introspect mode)
ProtectionMode type alias Literal["forms", "readOnly", "comments", "trackedChanges"]

docx_plus.comments

Anchored, threaded comments — the body-side range markers python-docx skips, the comment body in comments.xml, the thread graph in commentsExtended.xml, and — v0.5 — durable ids in commentsIds.xml plus author presence in people.xml. Architecture walkthroughs in Anchored comments and Durable ids and author presence.

Symbol Kind Notes
add_comment(target, text, *, author="", initials=None, id_registry=None, para_id_registry=None, durable_id_registry=None) function Anchor a comment to a Run, Paragraph (≥1 run required), or (Run, Run) tuple. Writes commentRangeStart / commentRangeEnd / the CommentReference marker run, the <w:comment> body, a w14:paraId stamp plus an unresolved <w15:commentEx> thread entry (v0.4), and a w16cid:durableId entry (v0.5). Does not write people.xml
reply_to_comment(doc, parent_id, text, *, author="", initials=None, id_registry=None, para_id_registry=None, durable_id_registry=None) function v0.4. Add a reply beneath an existing comment, mirroring the parent's anchor range. Raises CommentNotFoundError if parent_id is missing
resolve_comment(doc, comment_id) function v0.4. Mark the whole thread containing comment_id resolved (w15:done="1")
reopen_comment(doc, comment_id) function v0.4. The inverse — mark the thread unresolved
edit_comment(doc, comment_id, text) function Replace the body text of an existing comment in place. Preserves w:author / w:date / w:initials, the body-side anchors, and the w14:paraId that holds the thread together. Raises CommentNotFoundError if id missing
delete_comment(doc, comment_id, *, include_replies=True) function Remove every trace (range markers, reference run, body, thread entry). include_replies=True (default) also deletes the subtree, as Word does; False promotes orphaned replies to roots. Idempotent — missing id is a no-op
clear_all_comments(doc, *, remove_part=False) function Bulk delete every comment, thread entry, and durable id. remove_part=True tears down the comments, commentsExtended, and commentsIds parts. Leaves people.xml alone. Idempotent on an empty document
read_comments(doc) function List every comment paired with the document text it anchors. Returns list[AnchoredComment]
read_threads(doc) function v0.4. The same comments grouped into threads. Returns list[CommentThread]
CommentRef dataclass (frozen) comment_id, body_element — handle returned by add_comment
AnchoredComment dataclass (frozen) comment_id, author, initials, timestamp, text, anchored_text, paragraph_index, parent_id, resolved, durable_id
CommentThread dataclass (frozen) v0.4. root, replies, resolved
CommentIdRegistry(doc) class Per-document comment-id allocator. Subclasses the internal _IdRegistryBase and seeds from the comments part + any orphaned body anchors
DurableIdRegistry(doc) class v0.5. Per-document w16cid:durableId allocator, seeded from commentsIds.xml alone. Hex, not decimal — use next_hex(). Verified against a Word-authored file
set_author_presence(doc, author, *, provider_id="None", user_id=None) function v0.5. Write an author's people.xml entry. Idempotent per author; an empty name is a no-op and creates no part. user_id defaults to author. Opt-inadd_comment never calls it, since a fabricated directory identity is worse than an absent one
read_author_presence(doc) function v0.5. list[AuthorPresence] in document order. [] when the part is absent
clear_author_presence(doc, *, remove_part=False) function v0.5. Drop every author entry. Not wired into delete_comment — Word keeps stale authors, and pruning needs the author ref-counted across surviving comments
AuthorPresence dataclass (frozen) v0.5. author (the only join to comments.xml), provider_id, user_id
LOCAL_PROVIDER constant v0.5. "None" — what Word writes for an author with no directory behind them
CommentNotFoundError exception Dual-bases: DocxPlusError, KeyError. edit_comment / reply_to_comment / resolve_comment on a missing id
CommentTarget type alias Run | Paragraph | tuple[Run, Run]

docx_plus.layout

Page-layout extras — columns, mid-document section breaks, doc-level distinct even/odd headers. Architecture walkthrough in Layout.

Symbol Kind Notes
set_columns(section, num, *, space=720, separator=False, widths=None) function Emit <w:cols> into the section's sectPr. Idempotent (replaces existing). widths for unequal columns
insert_section_break(paragraph, *, start_type="nextPage") function Split sections at a chosen paragraph. Clones the trailing sectPr, sets <w:type>. Returns a Section proxy wrapping the new section
enable_distinct_even_odd_headers(doc) function Write <w:evenAndOddHeaders/> into settings.xml. Idempotent. Distinct from per-section titlePg (which python-docx already exposes)
disable_distinct_even_odd_headers(doc) function Remove the element. Idempotent
set_line_numbering(section, *, count_by=1, restart="newPage", start=1, distance=None) function Emit <w:lnNumType> for marginal line numbers. Idempotent, schema-strict (lands in its ECMA-376 17.6.17 slot)
set_page_borders(section, *, top=None, bottom=None, left=None, right=None, offset_from="page") function Emit <w:pgBorders> from one Border per side. All-None removes the element. Idempotent, schema-strict. offset_from says what Border.space is measured from
OffsetFrom type alias Literal["page", "text"]"page" (default) measures Border.space from the page edge, what Word's UI emits; "text" measures from the body text edge, the spec's implicit default when the attribute is omitted, producing a tight inner box
Border dataclass (frozen) One side of a page border: style, size (eighths of a point), color (RGB hex or "auto"), space (twips from text)
SectionStartType type alias Literal["nextPage", "continuous", "evenPage", "oddPage", "nextColumn"]
LineNumberRestart type alias Literal["newPage", "newSection", "continuous"]

docx_plus.bookmarks

Bookmarks and cross-references — paired body markers plus REF / PAGEREF complex fields. Architecture walkthrough in Bookmarks and cross-references.

Symbol Kind Notes
add_bookmark(target, name, *, id_registry=None) function Wrap target with <w:bookmarkStart> / <w:bookmarkEnd>. Validates name against [A-Za-z_][A-Za-z0-9_]{0,39}
delete_bookmark(doc, name) function Remove every bookmark with the given name. Idempotent
read_bookmarks(doc) function List every bookmark paired with its anchored text. Returns list[BookmarkInfo]
add_cross_reference(paragraph, *, bookmark, kind="text", hyperlink=True, number=None, position=False, suppress_non_delimiters=False, numeric_format=None, preserve_formatting=False) function Append a REF (kind="text") or PAGEREF (kind="page") complex field. \h appended by default. v0.5 added the switch surface: number\n / \r / \w (paragraph number), position\p ("above"/"below"), numeric_format\#, preserve_formatting\* MERGEFORMAT. bookmark is validated. Pair with mark_fields_dirty
NumberContext type alias v0.5. "plain" / "relative" / "full" for number above
BookmarkRef dataclass (frozen) bookmark_id, name, start_element, end_element
BookmarkInfo dataclass (frozen) bookmark_id, name, anchored_text, paragraph_index
BookmarkIdRegistry(doc) class Per-document bookmark-id allocator. Re-export of core.BookmarkIdRegistry since v0.5
BookmarkNameRegistry(doc) class v0.5. Per-document bookmark-name allocator. next_ref_name() mints hidden _Ref anchors
DuplicateBookmarkNameError exception Dual-bases: DocxPlusError, ValueError
BookmarkTarget type alias Run | Paragraph | tuple[Run, Run]
CrossReferenceKind type alias Literal["text", "page"]

docx_plus.notes

Footnotes and endnotes — insert-only API for v0.2. Architecture walkthrough in Footnotes and endnotes.

Symbol Kind Notes
add_footnote(paragraph, text, *, id_registry=None) function Append the body-side FootnoteReference marker run and the <w:footnote> body in footnotes.xml. Returns FootnoteRef
add_endnote(paragraph, text, *, id_registry=None) function Same shape as add_footnote but for endnotes
edit_footnote(doc, note_id, text) function Replace the body text of an existing footnote in place. Reserved ids (-1, 0) raise ValueError; missing ids raise NoteNotFoundError
edit_endnote(doc, note_id, text) function Same shape as edit_footnote but for endnotes
read_footnotes(doc) function List user-authored footnotes. Returns list[NoteContent]; separator entries (ids -1 / 0) are filtered out
read_endnotes(doc) function Same shape as read_footnotes
FootnoteRef dataclass (frozen) note_id, body_element
EndnoteRef dataclass (frozen) note_id, body_element
NoteContent dataclass (frozen) note_id, text, paragraph_index
FootnoteIdRegistry(doc) class Per-document footnote-id allocator. Ids -1 / 0 are reserved by Word and refused at reserve time (range check)
EndnoteIdRegistry(doc) class Per-document endnote-id allocator. Same reserved-id treatment
NoteNotFoundError exception Dual-bases: DocxPlusError, KeyError. edit_footnote / edit_endnote on a missing id

docx_plus.numbering

Custom list definitions — v0.5. python-docx has no CT_AbstractNum and no CT_Lvl, so it cannot express what a list looks like. OOXML splits a list into a <w:abstractNum> definition and a <w:num> instance; paragraphs reference the instance, and that indirection is what makes restarting possible. See Custom numbering.

Symbol Kind Notes
LevelDefinition dataclass (frozen) One outline level: fmt, text, start, indent, hanging, justify, suffix, restart_after, font. Validated against the ECMA-376 simple types at construction
define_list_definition(doc, *, levels, name=None, style_link=None, num_style_link=None, multi_level_type=None, num_registry=None, abstract_registry=None) function The primitive. Writes one w:abstractNum plus one w:num; returns the numId
define_bullet_list(doc, *, levels=1, indent_step=720, hanging=360, ...) function Preset using Word's round / hollow-o / square glyph cycle, each with its symbol font
define_numbered_list(doc, *, levels=1, indent_step=720, hanging=360, ...) function Preset using Word's 1. / a. / i. format cycle
apply_list(paragraph, num_id, *, level=0) function Write w:numPr. Idempotent; does not validate num_id (a dangling reference is legal and renders unnumbered)
remove_list(paragraph, *, suppress_style_numbering=False) function Drop the w:numPr. The flag writes the numId="0" sentinel instead, the only way to suppress numbering a style applies
restart_list(paragraph, num_id, *, level=0, start=1, num_registry=None) function Begin a fresh sequence: adds a second w:num over the same w:abstractNum with a w:startOverride, applies it, returns the new numId
read_list_definitions(doc) function Every definition in numbering.xml. Returns [] when the part is absent; never creates it. Note a fresh Document() already has nine from python-docx's template
ListDefinition dataclass (frozen) num_id, abstract_id, levels, name, style_link, num_style_link, multi_level_type, start_overrides
ListLevel dataclass (frozen) Read-side level. Every field is None when its element is absent — which Word reads as its own default, not as zero
NumIdRegistry(doc) class w:numId allocator, from 1. Allocates lowest-free via next_sequential(), matching Word
AbstractNumIdRegistry(doc) class w:abstractNumId allocator, from 0 — the one id namespace where zero is legal
Suffix type alias Literal["tab", "space", "nothing"] — what LevelDefinition.suffix accepts: the w:suff between the number and the text
Justification type alias Literal["left", "center", "right", "start", "end"]LevelDefinition.justify, the w:lvlJc of the number
MultiLevelType type alias Literal["singleLevel", "multilevel", "hybridMultilevel"]define_list_definition(multi_level_type=...); derived from the level count when None
InvalidLevelError exception Dual-bases: DocxPlusError, ValueError. A bad numFmt, an over-deep %N placeholder, more than nine levels; also (v0.6.2) what apply_list / restart_list raise for a level outside 0–8 or a negative start (was a bare ValueError)
ListDefinitionNotFoundError exception Dual-bases: DocxPlusError, KeyError. restart_list on an unknown numId
MAX_LEVELS int 9 — ECMA-376 caps w:lvl per definition

docx_plus.publishing

Long-document publishing primitives — Table of Contents, captions, Table of Figures. Each helper emits a complex field; pair with docx_plus.fields.mark_fields_dirty so Word populates the result on next open. Architecture walkthrough in Publishing.

Symbol Kind Notes
add_toc(paragraph, *, levels=(1, 3), hyperlink=True, page_numbers=True) function Append a TOC complex field. Instruction string matches Word's default ("Insert → Table of Contents") with \o, \h, \z, \u, optional \n switches
add_caption(paragraph, label, *, caption_type="Figure", numbering="ARABIC", bookmark_name=None, bookmark_id_registry=None) function Label text run + SEQ <caption_type> \* <numbering> complex field. caption_type must match the \c switch on a downstream Table of Figures. v0.5: bookmark_name brackets the label + number in a bookmark, which is the only way to make the caption referenceable — a REF cannot target a SEQ field
add_table_of_figures(paragraph, *, caption_type="Figure", hyperlink=True) function Append a TOC \c "<caption_type>" complex field that collects matching captions

docx_plus.tables

Table appearance — the half python-docx omits. It models rows, columns, cells, widths, and a working _Cell.merge, but has no CT_Border, CT_TblBorders, CT_TcBorders, or CT_Shd class and registers none of those tags. New in v0.5. Architecture walkthrough in Table formatting.

Symbol Kind Notes
set_table_borders(table, *, all_edges=None, top=None, bottom=None, left=None, right=None, inside_h=None, inside_v=None) function Write <w:tblBorders>. Full replacement, not a merge; naming no edges removes the element. all_edges covers all six; an explicit edge overrides it. Border.space is ignored — Word writes w:space="0" on tables and its UI cannot produce anything else
set_cell_borders(cell, *, all_edges=None, top=None, bottom=None, left=None, right=None, tl2br=None, tr2bl=None) function Write <w:tcBorders>. Same semantics. all_edges covers the four sides only — the diagonals are a "crossed-out cell" mark, never what a caller means by "all borders"
Shading(fill="auto", pattern="clear", color="auto") dataclass Frozen. fill is the background, pattern an ST_Shd value drawn over it, color that pattern's foreground. A solid fill is the default pattern="clear" with only fill set. Validates all three at construction
set_table_shading(table, shading) function Write <w:shd> on <w:tblPr>. None removes it
set_cell_shading(cell, shading) function The same on <w:tcPr>
set_row_shading(row, shading) function CT_TrPr has no w:shd child — there is no row-level shading in the format. Writes through to every cell, as Word does. Iterates <w:tc> elements, not Row.cells, so a spanning cell is visited once
shading_attrs(shading) function Serialize to the CT_Shd attribute mapping
merge_cells(start, end) function Thin wrapper over _Cell.merge, translating InvalidSpanError into InvalidMergeError. Returns the top-left cell of the region, which need not be start
unmerge_cell(cell) function The inverse, which python-docx lacks entirely — nothing in it removes a w:gridSpan or w:vMerge. Works from any cell in the region including a vertical continuation. Content stays in the anchor; widths divide evenly, since the originals were summed away by the merge. Idempotent
normalize_horizontal_merges(table, *, discard_content=False) function Rewrite legacy <w:hMerge> spans as <w:gridSpan>, which is the only form python-docx's grid model understands. Rendering-preserving (verified against Word 2016). Refuses by default to drop text in a continuation cell — invisible in Word, so keeping it would surface hidden content. Returns the number of regions converted
read_table_formatting(table) function TableFormatting — style id, table borders/shading, and a CellFormatting per <w:tc>. Direct formatting only; the table-style cascade is not resolved, so a Table Grid table reads back with no borders
TableFormatting / CellFormatting dataclass Frozen. CellFormatting.column is a grid offset, not an index into Row.cells. One entry per <w:tc>, so a merged cell appears once
InvalidMergeError exception DocxPlusError + ValueError

docx_plus.revisions

Tracked changes — read, author, and resolve OOXML revision marks (w:ins / w:del / move wrappers / property-change markers). python-docx cannot read or write tracked changes at all; this module fills the gap. Scoped in ROADMAP.md §1 at the repo root.

Symbol Kind Notes
enable_track_changes(doc) function Write <w:trackChanges/> into settings.xml so Word records every subsequent user edit as a revision. Idempotent (normalises a pre-existing element to "on", collapses duplicates)
disable_track_changes(doc) function Remove every <w:trackChanges/>. Idempotent. Existing body revision marks are untouched
mark_insertion(target, *, author="", date=None, id_registry=None) function Wrap existing run(s) in <w:ins>. date=None stamps current UTC (ms precision). Returns RevisionRef
mark_deletion(target, *, author="", date=None, id_registry=None) function Wrap existing run(s) in <w:del> and retag each <w:t> to <w:delText>. Returns RevisionRef
read_revisions(doc) function Enumerate every revision in document order, each paired with its metadata and affected text. Returns list[TrackedChange]
accept_revision(doc, revision_id) function Accept the revision(s) carrying revision_id, keeping the recorded edit. Raises RevisionNotFoundError if absent
reject_revision(doc, revision_id) function Reject the revision(s) carrying revision_id, restoring the prior state. Raises RevisionNotFoundError if absent
accept_all_revisions(doc) function Accept every tracked change. Idempotent; resolves innermost-first
reject_all_revisions(doc) function Reject every tracked change. Idempotent; resolves innermost-first
RevisionRef dataclass (frozen) Write-side handle: revision_id, body_element (the <w:ins> / <w:del> element)
TrackedChange dataclass (frozen) Read-side result: revision_id, revision_type, author, timestamp, text, paragraph_index
RevisionIdRegistry(doc) class Per-document revision-id allocator. All revision types share one w:id namespace; seeds from every revision-bearing element in the body
RevisionType type alias Literal["insertion", "deletion", "move_from", "move_to", "format_run", "format_paragraph", "paragraph_mark_insertion", "paragraph_mark_deletion"]
RevisionTarget type alias Run | Paragraph | tuple[Run, Run] — same target shapes as add_comment; a range must lie within one paragraph
RevisionNotFoundError exception Dual-bases: DocxPlusError, KeyError. accept_revision / reject_revision on a missing id

docx_plus.lint

v0.6. Audit a document for formatting defects, and describe what repairing them would change. A composing layer, not a capability module: like cli/ it sits above the capability packages and reads across them, adding no OOXML knowledge of its own — every judgement is built on styles/'s cascade resolver and the document sweep. Nothing here writes: lint reports and plan_fixes plans, and applying a plan is deferred to v0.7. Full reference in reference/lint.md.

Symbol Kind Notes
lint(doc, *, select=None, exclude=None, include_tables=True, profile=None) function Sweep the document once and run the selected rules. Returns list[Finding] sorted by severity, then document order. select / exclude take rule ids or tags; naming a tag also enables that cluster's off-by-default rules. Body only — headers, footers, notes, and comments are not swept
plan_fixes(findings, *, allow_content=False) function Turn findings into an ordered, serializable FixPlan. Decides the three things no individual rule can: applying order (deletions last, back to front), the content gate, and conflicts between edits
Finding dataclass (frozen) rule, kind, severity, message, location, observed, expected, fix, adds_content; fixable and sort_key properties
Issue dataclass (frozen) What a rule body yields — only what the rule itself knows. The engine promotes each to a Finding by stamping on the id / kind / severity from the registration, so a rule cannot advertise one severity and emit another
Location dataclass (frozen) paragraph_index, run_index, style_id, excerpt; all optional, because a finding about a style definition has no paragraph to point at. describe() renders whichever it has. paragraph_index is 0-based and counts table-cell paragraphs
Fix dataclass (frozen) summary, safety, operations — what a rule would do about what it found, described and never executed
FixOperation dataclass (frozen) op + JSON-serializable args. to_dict()
FixOp type alias The closed vocabulary: clear-run-properties, clear-paragraph-properties, clear-paragraph-numbering, set-run-language, replace-paragraph-text, delete-paragraph, delete-style. Named operations rather than callables, so a plan survives being written to a file and applied by another process
FixSafety type alias Literal["safe", "review", "destructive"] — how recoverable applying it is. Orthogonal to adds_content, which is about content vs formatting
FixPlan dataclass (frozen) fixes, deferred, conflicts, unfixable. Every finding lands in exactly one, so a plan accounts for the whole audit. operations flattens the kept fixes into applying order; to_dict() serializes
PlannedFix dataclass (frozen) One kept or withheld fix paired with the finding that produced it. rule / safety / adds_content / operations / deletes properties
FixConflict dataclass (frozen) kept, dropped, reason — two edits claimed the same run property or overlapping text spans; the earlier one wins
Profile dataclass (frozen) Per-rule enable / disable and severity overrides — the one place a house opinion may live. load() takes a path, a mapping, or None; discover(start) walks up for docx-plus-lint.json. A profile may not configure a tag
RuleSettings dataclass (frozen) enabled, severity, options for one rule. options reach the rule through LintContext.option (v0.6.2); the rule validates the value, the profile only promises it is JSON
DEFAULT_PROFILE_NAME str "docx-plus-lint.json" — what Profile.discover looks for
Rule dataclass (frozen) A registered rule: id, kind, severity, description, check, tags, default_on. matches(selector)
rule(*, id, kind, severity, description, tags=(), default_on=True) decorator Register a check function. Rules register at import, so a new one is a single function
all_rules() function Every registered rule, in id order
select_rules(select, exclude, profile=None) function Resolve selectors to rules. Precedence: profile adjusts the defaults, explicit select overrides both, exclude is applied last
LintContext dataclass What a rule is given: doc and paragraphs (the whole swept document, so comparative rules are possible), plus resolve(target, stop_below=...) and excerpt(paragraph_index). v0.6.2 adds profile (the loaded Profile, empty by default) and option(rule_id, key, default) — one rule-specific value from the profile's options block, or the rule's own default
CheckFn type alias v0.6.2. Callable[[LintContext], Iterator[Issue]] — the shape of a rule body, what @rule decorates
RuleKind type alias Literal["consistency", "structural", "policy"] — the distinction that keeps an opinionated feature inside a lean library. No policy rule ships enabled
Severity type alias Literal["error", "warning", "info"]
UnknownRuleError exception Dual-bases: DocxPlusError, KeyError. A selector, or a profile, named a rule id or tag that does not exist
InvalidProfileError exception Dual-bases: DocxPlusError, ValueError. The profile is unreadable or malformed
InvalidFixError exception Dual-bases: DocxPlusError, ValueError. A rule produced a fix plan_fixes cannot order safely — one that both deletes and does positional work

Twenty rules ship, sixteen on by default; eleven are deliberately report-only, because their repair is a judgement the document cannot supply. docx-plus lint --list-rules prints the catalogue.

Three options are read today (v0.6.2): font-outliers honours max_share (default 0.05) and max_runs (default 5), and manual-heading-formatting honours max_chars (default 80). A {"rules": {"font-outliers": {"options": {"max_share": 0.02}}}} profile tightens the first without touching the rule's code.

docx_plus.cli

The docx-plus command-line interface — a thin shell over the library (each subcommand wraps one tested function). Full reference, including every subcommand and flag, lives in cli.md.

Symbol Kind Notes
main(argv=None) function Console entry point (docx-plus = "docx_plus.cli:main"; also python -m docx_plus.cli). Returns 0 on success, 1 on a handled library/CLI error, 2 when no command was given
build_parser() function Construct the top-level argparse.ArgumentParser with every subcommand registered
docx-plus skill path\|list\|show\|install command v0.5. Locate, read, or install the agent skill packaged at docx_plus/skill/. The one command that touches no .docx, so it takes --dest / --user / --force rather than -o/--output
docx-plus lint FILE command v0.6. Report formatting defects. --rule / --exclude (id or tag, repeatable), --no-tables, --profile / --no-profile, --list-rules, --json. Read-only, and exits 1 when it found anything, so it gates a CI step directly
docx-plus plan FILE command v0.6. Describe the repair — which edits, in what order, which are withheld for changing content, and which collide. --allow-content includes the withheld ones. Also read-only: this release applies nothing

Internal modules (not part of the public API)

These exist in source but are deliberately not re-exported from the top-level package.

docx_plus._testing.ooxml_asserts

Shared test-suite assertion helpers. Internal — referenced from tests/ only.

Symbol Notes
assert_ids_unique(doc) Every w:id on w:sdt descendants is unique
assert_style_defined(doc, style_id) w:style[@w:styleId=...] exists in word/styles.xml
count_controls(doc, control_type=None) Count SDTs in the body; filter by "text"/"dropdown"/"combobox"/"date"/"checkbox"
assert_protected(doc, mode=None) w:documentProtection present with w:enforcement="1"; optionally validates w:edit
assert_field_dirty(doc) w:updateFields val="true" present in settings.xml
assert_field_not_dirty(doc) settings.xml carries no w:updateFields element
field_instruction_text(p_element) The concatenated <w:instrText> text in a paragraph element, or None
assert_para_ids_unique(doc) Every w14:paraId in the package (body plus side parts) appears exactly once
assert_durable_ids_well_formed(doc) commentsIds.xml satisfies the invariants Word relies on
assert_numbering_well_formed(doc) numbering.xml satisfies the invariants Word relies on

The SPEC §10 list is populated, and the v0.4–v0.5 side-part helpers have joined it; assert_style_not_defined and assert_no_orphan_relationships remain unwritten (no caller needs them yet — see TEST_GAPS.md N1).


Conventions

  • Units. font_size in points (float). Spacing, indent, line height in twips (int) unless line_spacing_rule == "auto", in which case line_spacing is a multiplier (e.g. 1.15). Colors as "RRGGBB" uppercase hex strings without #.
  • Toggle properties (bold, italic, caps, small_caps, strike, vanish). True writes the element with no w:val. False writes w:val="false". None (in modify_style) removes the element so the inherited value resumes. See The cascade resolver.
  • Identifiers. Style IDs (w:styleId) — machine-readable, what every function takes. Style names (w:name) — human-readable, what Word's UI shows. The library accepts IDs everywhere; names are a reconciliation concern handled by find_matching_style / remap_styles.