docviews
Read .docx in original, accepted, and markup views across text-bearing parts; author common content; zero runtime dependencies.
Documented-API parity: verified against python-docx 1.2.0 within the agreed
boundary: documented public behavior is differentially tested, while
undocumented lxml, OOXML, and OPC internals are excluded. The source-backed
584-row matrix records 578 tested behavior matches, 0 partial/not-assessed
in-scope rows, and 6 structural/internal scope exclusions. All 471 runtime
enum members match; the documented-only MSO_THEME_COLOR_INDEX.MIXED is
implemented with the published value -2. The full 239-test suite passed on
each tested CPython patch release from 3.9.25 through 3.15.2. See
API_PARITY_MATRIX.md for scope and evidence. Stable
release metadata 1.0.0 is selected, but no package has been uploaded; PyPI
publication still requires explicit maintainer approval.
from docviews import open_docx
doc = open_docx("contract.docx")
doc.text("accepted") # what Word shows with changes accepted
doc.text("original") # what Word shows with changes rejected
doc.text("markup") # everything the file contains
for c in doc.comments():
c.anchor_text # the exact characters under review
for r in doc.revisions():
r.kind, r.author, r.scope # scope: text | paragraph-mark | row | block
Writing is the same object:
from docviews import Document, Inches
doc = Document.new() # or open_docx("existing.docx")
doc.add_heading("Q3 Report", 0)
para = doc.add_paragraph("Revenue grew ")
para.add_run("18%").bold = True
doc.add_table(rows=2, cols=2, text=[["Region", "Revenue"], ["EMEA", "1,204,332"]])
doc.add_picture("chart.png", width=Inches(5)) # media part + relationship, wired up
doc.sections[0].footer.add_paragraph("Confidential")
doc.save("q3.docx") # atomic: never a half-written file
Already written against python-docx? If your code imports the top-level
docx module and uses the supported documented object API, start with this
change:
import docviews.compat as docx # instead of: import docx
If it also imports docx.shared or docx.enum.*, change those import paths to
docviews.compat.shared and docviews.compat.enum.*. Documented proxy classes
also have mirrored modules under docviews.compat; the full path inventory is
in API_PARITY_MATRIX.md. The documented public API
matrix records 578 tested behavior matches and no partial or unassessed
in-scope behaviors; six structural/internal entries are excluded by the agreed
boundary. The documented-only MIXED enum value is implemented separately from
the upstream runtime omission. This is parity for the documented contract, not
for undocumented internals or malformed states outside that contract.
Then a CLI that tells you what your current reader is losing:
$ docviews audit contracts/
file verdict revs unreach expand why
------------------------------------------------------------------------------
tracked-changes.docx lossy 7 0 6x 7 paragraphs change under acceptance (+5 inserted, -68 deleted characters)
tracked-tables.docx lossy 0 0 6x 9 table rows marked deleted (no text reader reports this)
un-report.docx lossy 0 9629 6x text no python-docx API returns: footnotes 9,629 chars
strict.docx lossy 0 0 4x ISO/IEC 29500 Strict package (python-docx raises on these)
expansion-bomb.docx lossy 0 0 59x expands 59x (28.5 MB part)
5 files: 5 lossy (100.0%), 0 unreadable, 66 numbers in unreachable text
Why this exists
Many Python document pipelines rely on python-docx (95.4M downloads/month in a PyPI measurement on 2026-10-14). But paragraph.text is not always the full document. Measured over 832 public documents (method and raw data in CENSUS.md):
| what happens | how often | why |
|---|---|---|
| text returned matches neither Word view | 10 of 22 documents containing revisions | .text drops inserted text (a rejected behaviour) and deleted text (an accepted behaviour) at the same time |
| 147,000 characters sit in parts no API can reach | 152 documents | footnotes, endnotes and text boxes have no reader |
| a deleted table row or paragraph mark goes unreported | 121 of 255 revision containers, 20 documents | the markup sits in w:trPr / w:pPr/w:rPr, not around runs — no Python text reader reports it |
| the file cannot be opened at all | 75 of 832 (9.0 %), of which 61 ISO-Strict |
the Transitional relationship type is hardcoded |
| a 0.49 MB file costs 416 MB of RAM | 23 documents expand >20× | every part is parsed eagerly and kept |
And saving through it silently rewrites the package — measured by opening all 832 documents, saving each one unchanged, and diffing the two packages:
| open → save, unchanged | python-docx 1.2.0 | docviews 1.0.0 metadata (local, unpublished) |
|---|---|---|
| documents opened | 762 / 832 | 831 / 832 |
| documents that come back with the same package entries | 523 | 831 |
| documents with every part byte-identical | 7 | 831 |
| documents losing parts on save | 239 | 0 |
| parts byte-identical after save | 5,398 / 12,042 | 13,912 / 13,912 |
The parts python-docx drops are not decoration: 232 documents lose footnotes.xml.rels and endnotes.xml.rels, 231 lose fontTable.xml.rels, and directory entries vanish. The prose usually survives that; the wiring around it does not always, and nobody chose to lose it. docviews' rule is simpler: a part the caller did not edit is copied out of the package byte-for-byte.
The paragraph-level proof, from docx-revisions' own test fixture:
the document offers exactly two readings:
accepted : "We prefer color spelling."
rejected : "We prefer colour spelling."
python-docx : "We prefer spelling." ← a sentence Word cannot display
docviews : the two readings above
Install
Stable-release metadata 1.0.0 has been selected, but the distribution is
not yet published. For the current local source checkout, install directly from
the project root:
python -m pip install .
The runtime package has no dependencies and the documented-API test suite is
verified on CPython 3.9–3.15. Do not expect pip install docviews==1.0.0 to
work until the maintainer explicitly authorizes and completes publication.
Reading
doc.text("original") # reject all changes
doc.text("accepted") # accept all changes
doc.text("markup") # show everything, deletions included
for p in doc.paragraphs(): # body, headers, footers, footnotes, endnotes, comments
p.text, p.style.name, p.original_text, p.alignment, p.is_revised, p.part_name
for run in p.runs:
run.text, run.bold, run.italic, run.font.size, run.font.color
doc.iter_paragraphs() streams the same walk at flat memory; doc.stream() walks the body alone; doc.parts(), doc.revisions(), doc.comments(), doc.numbers() are described below.
Revisions that are not runs
Word marks a deleted table row with w:del inside w:trPr, and a deleted paragraph mark with w:del inside w:pPr/w:rPr — the row disappears, two paragraphs merge, and not one Python text reader reports either:
for r in doc.revisions():
r.kind, r.author, r.date, r.scope # scope: text | paragraph-mark | row | block
9 tracked changes in tracked-tables.docx
delete Watertight 2026-10-02T09:00 id=9201 scope=row
Comments, with the characters they are attached to
python-docx 1.2.0 ships comment text and its release notes say the "reference" range "is not currently planned". A comment without its range is a note with no idea what it is about:
for c in doc.comments(): # anchors move with the view you ask for
c.author, c.text, c.resolved, c.is_reply, c.reply_ids
c.anchors[0].paragraph_index, c.anchors[0].start, c.anchors[0].end
c.anchor_text # the exact characters under review
Across the census corpus: 999 comments in 89 documents, 1,189 anchors, every anchor verified against its own view text.
Writing
New documents
doc = Document.new()
doc.add_heading("Report", 0) # 0 = Title, 1..9 = Headings
doc.add_paragraph("Body.", style="Heading 2", align="center")
doc.add_list_item("bullet", kind="bullet") # or kind="number", level=1
doc.add_page_break()
table = doc.add_table(rows=3, cols=2, text=[["Region", "Revenue"], ["EMEA", "1,204,332"]])
table.cell(0, 0).paragraphs[0].runs[0].bold = True
table.add_row()
doc.save("report.docx")
Formatting is settable per run — run.bold, .italic, .underline, .size (EMU, so Pt(12) works), .font_name, .color = "FF0000" — and readable back the same way.
Editing documents you did not create
doc = open_docx("contract.docx")
para = doc.paragraphs()[3]
para.set_text("replaced wording") # keeps the paragraph's style
para.insert_after("a new clause") # Word's Enter key
para.runs[0].bold = True
doc.add_paragraph("Added by legal.")
doc.save("contract-v2.docx") # everything else copied verbatim
The guarantee is not a promise, it is a measurement: open all 832 corpus documents, add a paragraph, save, and every part except word/document.xml comes back byte-identical — tracked-change markup, comments and their anchors, footnotes, endnotes, text boxes, images, custom XML, theme, numbering.
That includes the two things an editor usually breaks first:
- Tracked changes stay tracked. Adding text to a document under review does not accept or reject anything.
- Comments keep their ranges. A comment whose anchor sits in a paragraph you edited still points at the same characters, because the anchors are re-read from the saved package, not remembered.
Pictures, headers, footers, merged cells, styles, sections
doc = Document.new()
doc.add_picture("chart.png", width=Inches(5)) # aspect ratio follows
doc.inline_shapes # [InlineShape('Picture 1', 5.00x... in, word/media/image1.png)]
doc.sections[0].header.add_paragraph("Confidential — draft") # part created on the spot
doc.sections[0].footer.add_paragraph("Page 1 of 1")
table = doc.add_table(rows=3, cols=3, text=[["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]])
table.cell(0, 0).merge(table.cell(0, 2)) # w:gridSpan on the first cell
table.cell(1, 0).merge(table.cell(2, 0)) # w:vMerge restart/continue
table.cell(1, 0).text # "d\ng" — the merge is followed, not blanked
doc.styles.add_style("Callout", "paragraph", base="Normal", bold=True, size=Pt(12))
doc.add_paragraph("Read this.", style="Callout")
doc.add_section() # a second section
doc.core_properties.title = "Q3 Report" # docProps/core.xml
doc.save("q3.docx")
Writing a comment — and, unlike python-docx, knowing what it points at:
run = doc.add_paragraph("The total is 1,204,332 units.").runs[0]
comment = doc.add_comment([run], text="Check against the ledger.",
author="AM", initials="AM")
comment.anchor_text # "1,204,332 units." — the range, not just the note
doc.comments() # and it reads back with its anchors from the file
Page furniture beyond the default header, styles with their inheritance, and
word/settings.xml:
section = doc.sections[0]
section.first_page_header.add_paragraph("Cover page only") # sets w:titlePg
section.even_page_header.add_paragraph("Even pages") # sets w:evenAndOddHeaders
section.orientation = WD_ORIENT.LANDSCAPE # w:orient + swapped sides
section.start_type = WD_SECTION_START.ODD_PAGE # w:type
section.header.add_table(rows=1, cols=2, text=[["a", "b"]]) # tables in headers
style = doc.styles["Heading 1"] # lookup by name, id or default
style.base_style, style.next_paragraph_style # the inheritance chain
style.quick_style, style.priority, style.hidden = True, 5, False
doc.styles.latent_styles.add_latent_style("Callout", ui_priority=10)
doc.settings.odd_and_even_pages_header_footer = True
Images are measured from their own bytes — PNG, JPEG, GIF, BMP, TIFF, with the DPI the file declares — so this library still has zero dependencies and will not insert a picture at a guessed size. SVG is refused with a message that says what to do instead.
Everything above is checked against python-docx: it opens the saved file and sees the same pictures, the same header text, the same merged cells and the same style. tests/test_images.py and tests/test_authoring.py are that check.
Writing is honest about what it does not do
Block-level revision re-flow (a whole paragraph wrapped in w:ins) is reported but not yet applied to the views (0 instances in the 832-document corpus). Fields, symbol-font symbols, .doc input and rendering are not there. Floating/anchored images are read but not written — add_picture inserts inline pictures, the kind a report or a template needs. Nothing above is silently approximated.
Replacing python-docx in your code
For the complete import map, documented API scope, and migration notes, see
MIGRATION.md
and API_PARITY_MATRIX.md.
Contributor and test instructions are in
CONTRIBUTING.md.
The compatibility surface matches python-docx 1.2.0's documented public API;
imports must be redirected to docviews.compat, and undocumented internals are
outside the parity claim.
import docviews.compat as docx # replace `import docx`
from docviews.compat.enum import WD_ALIGN_PARAGRAPH # update enum/shared paths too
doc = docx.Document()
doc.add_heading("Quarterly Report", 0)
p = doc.add_paragraph("Total: ")
p.add_run("1,204,332").bold = True
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
t = doc.add_table(rows=3, cols=2)
t.style = "Table Grid"
t.cell(0, 0).text = "Region"
doc.sections[0].left_margin = docx.Inches(1.2)
doc.core_properties.author = "Finance"
doc.save("out.docx")
docviews.compat provides python-docx's shapes — Document(), add_paragraph/add_heading/add_run/add_table/add_page_break, add_picture, paragraphs, tables, sections[0].header/.footer, core_properties, styles.add_style, inline_shapes, style.name, run.font.bold, Pt/Inches/Cm/RGBColor, WD_ALIGN_PARAGRAPH — on top of this reader, so you keep the anchors, the views, the scopes and the byte-exact save. examples/switch_check.py is the acceptance test: the same script run against python-docx and against the shim prints identical output (run and diffed locally; see CENSUS.md §6.3).
Two deliberate file-handling departures:
Document(path)opens ISO-Strict packages (61 of the census files), which python-docx cannot open at all.save()copies unedited parts verbatim instead of re-serialising every part it parsed.
All 471 enum members present in python-docx 1.2.0 match in value, str(),
repr(), and xml_value. The published enum page also lists
MSO_THEME_COLOR_INDEX.MIXED, which the pinned runtime omits; docviews includes
it with the Microsoft Office API value -2 and records the docs/runtime
conflict in COMPAT.md.
COMPAT.md is the measured answer to "what is still missing": every public
attribute python-docx 1.2.0 puts on the objects it hands you is answered, and
the 471 enum members and 30 enum classes python-docx ships are importable
with the same values (WD_LINE_SPACING.DOUBLE == 2) — from
docviews.compat.enum, and from same-named submodules
(docviews.compat.enum.text, .style, .section, .table, .shape, .dml),
so the import line survives the swap, not just the name.
What is not claimed is the XML layer: docx.oxml (OxmlElement, qn,
element.xpath), doc.element and the OPC part classes. Code that reaches into
the lxml tree is not migrated by changing an import — that code is rewritten
against docviews.views, and COMPAT.md says so instead of pretending
otherwise.
Still not authorable anywhere in this library: floating/anchored shapes, charts, fields and symbol-font glyphs, tables of contents as objects. They are read and preserved byte-for-byte.
Comparison snapshot
COMPARE.md is the evidence-linked comparison, including methodology, caveats,
and a rated scorecard (support/adoption snapshot updated 2026-10-31). On that
rubric, docviews scores 8.20 vs 7.50 unweighted. python-docx still leads for
migrating an existing codebase (8.45 vs 7.45); docviews leads for a new
document pipeline (8.70 vs 6.95) and for reading/auditing (8.85 vs 6.65).
Those ecosystem scores do not substitute for the separate API matrix.
| measurement | python-docx 1.2.0 | docviews 1.0.0 metadata (unpublished) |
|---|---|---|
| downloads / month | 95.4M (checked 2026-10-14) | no published distribution; stable metadata selected 2026-11-01 |
| documented API parity | baseline | 578 in-scope behavior matches; 0 partial/not-assessed; 6 structural/internal scope exclusions; details in API_PARITY_MATRIX.md |
| bounded compatibility audit | baseline | 276/276 sampled object names, 78/78 sampled callable parameter shapes, 471/471 runtime enum members |
| unchanged open → save | 762 / 832 open; 7 keep every part | 831 / 832 open; 13,912 / 13,912 parts byte-identical |
| tricky-document reading | 14,640 numbers lost (15.9%) | reference reader; 0 lost by construction |
| runtime dependencies | lxml + typing_extensions | none |
| wheel size | 252,987 B | about 144 KiB (local stable build) |
A separate eight-document public-sector smoke set sits outside the 832-file
fixture corpus: docviews preserved all 248/248 package entries; python-docx
omitted two unreferenced entries in one template. This is package-preservation
evidence, not a text-rendering gold standard or a broad real-world sample; see
real_world_eval/REPORT.md.
Compatibility boundary
API_PARITY_MATRIX.md
is the source-backed documented-public-API inventory and differential record;
COMPAT.md is an additional bounded object-sample audit, not the full matrix.
The matrix records 578 tested behavior matches, 0 partial/not-assessed in-scope
entries, and 6 structural/internal exclusions. It also reports complete
availability, call-shape, writability, enum, and import-path checks within the
agreed documented scope. Undocumented docx.oxml, lxml, and OPC internals and
malformed states outside the published contract are not covered.
The full 239-test suite passed on each tested CPython patch release:
3.9.25, 3.10.22, 3.11.18, 3.12.16, 3.13.16, 3.14.10, and 3.15.2. The twelve
selected recipes from python-docx's own documentation produce matching
save-and-reopen transcripts. Wheel and sdist builds were separately clean-
installed and smoke-tested; see RELEASE_READINESS.md for artifact details.
For the import boundary and migration caveats, see MIGRATION.md,
COMPAT.md, and COMPARE.md.
Guardrails, not silence
A .docx is a zip, and a zip can lie about its size. docviews refuses parts that exceed explicit limits instead of exhausting memory, and every part it could not read is reported — never silently dropped:
from docviews import Limits
doc = open_docx("upload.docx", limits=Limits(max_part_bytes=8_000_000))
doc.warnings # ["skipped word/footnotes.xml: expands 900x (limit 400x)"]
doc.stream() walks the body with iterparse and holds one paragraph at a time. On the worst file in the census:
| reader | peak RSS | paragraphs | time |
|---|---|---|---|
| python-docx 1.2.0 | 416 MB | 4,735 | 2.7 s |
| docviews (tree) | 346 MB | 4,735 | 2.5 s |
| docviews (stream) | 15 MB | 4,735 | 2.4 s |
Command line
docviews audit PATH [--json] [--ci] [--compare python-docx|mammoth|docx2python]
docviews text FILE --view accepted|original|markup
docviews revisions FILE [--json]
docviews comments FILE [--roots-only] [--threads] [--json]
docviews parts FILE
--ci turns the audit into a build gate:
$ docviews audit contracts/ --ci --fail-on revisions,unreachable
--ci: 3 file(s) failed [revisions, unreachable] # exit code 2
What it does not do yet
- block-level revisions: a whole paragraph wrapped in
w:ins/w:delis reported (scope="block") but its text does not move between views yet (0 instances in the 832-document corpus, so it is measured, not guessed) - fields and symbol-font symbols — a
PAGEfield shows its cached result, and aw:symcharacter is not resolved to a glyph .doc(binary) input — the OLE container is not parsed; convert to.docxfirst- rendering — page geometry, pagination and layout; this library is about the content of a document, not its pixels
- floating and anchored shapes, charts, tables of contents as objects — they are read and preserved byte-for-byte, but there is no API to author them
Honesty rules this library follows
- Nothing is dropped silently. Anything unread lands in
warnings. - Views are exact functions of the file — no "best effort" text.
- Saving is not a loss. Parts nobody edited are copied byte-for-byte, and that is asserted over the corpus, not asserted in prose.
- Claims are measured.
COMPAT.mdis generated, not typed;CENSUS.mdis the method;tests/test_corpus.py,tests/test_comments.pyandtests/test_write.pyre-run it and fail if a number moves without the census moving too. Revisions and comments are additionally cross-checked against hand-written XML counters (verify_revisions.py).
Development
The runtime package has no dependencies. Install the optional test tools with
pip install -e ".[dev]"; the test extra pins the minimum compatible
python-docx version (1.2.0) used to verify interoperability.
The census-backed tests require a local corpus/ directory. Its third-party
fixture files are gitignored until their redistribution terms are reviewed; if
it is absent, those marked tests skip. To recreate it, collect the source test
suites and follow the commands in CENSUS.md using build_corpus.py.
python -m pytest tests/test_views.py tests/test_comments.py tests/test_write.py tests/test_cli.py
python -m pytest tests/test_corpus.py # census regression
python roundtrip_probe.py # save-every-file check
python compare_roundtrip.py # head-to-head vs python-docx
python compat_coverage.py # python-docx API coverage -> COMPAT.md
python authoring_impact.py # what each authoring call touches
python examples/switch_check.py --shim # the compat acceptance test
Project links
MIT licensed.
Metadata
Release files for docviews 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| docviews-1.0.0.tar.gz | 184.8 kB | With provenance | Details for docviews-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| docviews-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for docviews-1.0.0-py3-none-any.whl |
Total release size: 332.4 kB
Release files / docviews-1.0.0.tar.gz
| Download URL | docviews-1.0.0.tar.gz |
|---|---|
| Size | 184.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
376e010fcd64847cd97afded3208face68bf43380ccfedfcb108d64e77c88b34
|
|
BLAKE2b-256 checksum How to use checksums |
d53c651f76d2f106a042d8393a80212ba3330f262ba06919a20b03be220b8274
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / docviews-1.0.0-py3-none-any.whl
| Download URL | docviews-1.0.0-py3-none-any.whl |
|---|---|
| Size | 147.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6c3e8f5a4b892375afd465056c4211ed0fb0127a6bd4e3131bc86605af1f7e0a
|
|
BLAKE2b-256 checksum How to use checksums |
d4427c9efb396e2629a7d20b164b7f20c97bc00597272817c6e8c028085bb4cf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for docviews 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for docviews-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.