Open-source DOCX comparison tool to generate native Word tracked changes (redlines) in Python — compare two Word files and get back a third document showing every insertion, deletion, and (optionally) move as native Word tracked changes, without any MS Word dependency.
Comparing .docx documents has long been dominated by commercial software, with cost
barriers and little integration flexibility. Python-Redlines brings open-source .docx
redlining to the Python ecosystem so legal hackers, hobbyists, and product teams can build
on it freely: two documents in, one redline out.
🔗 Try it now: redlines.opensource.legal — a live browser demo of the comparison engine, no install required. See also the full documentation site and Python-Redlines vs. commercial alternatives (Draftable API, Cloudmersive, server-side MS Word automation).
The default engine is Docxodus — a modernized, actively-maintained .NET 10 comparison engine (detailed below). Install it and you're running; the engine binary is prebuilt and embedded in the wheel, so there is no .NET SDK to install and nothing to compile:
pip install python-redlines[docxodus]
from python_redlines import DocxodusEngine
with open("original.docx", "rb") as f:
original = f.read()
with open("modified.docx", "rb") as f:
modified = f.read()
engine = DocxodusEngine()
redline_bytes, stdout, stderr = engine.run_redline("Reviewer", original, modified)
with open("redline.docx", "wb") as f:
f.write(redline_bytes)That's the whole thing. The rest of this README covers the other engines, comparison settings, and how the packages are built and distributed.
Docxodus v11.0.0 removed WmlComparer, the algorithm this library ran by default for its
entire life. DocxDiff — introduced as an opt-in in 0.3.0, upstream's default since v8.0.0, and
now mature — replaces it. There is no flag to bring the old engine back: it no longer exists in
the binary.
If you never passed comparison settings, your code needs no change. Output will differ, because the algorithm differs; see Upgrading to 1.0.0.
Three keyword arguments were removed and now raise ValueError:
engine.run_redline("Reviewer", original, modified, engine="docxdiff") # ValueError
engine.run_redline("Reviewer", original, modified, detail_threshold=0.3) # ValueError
engine.run_redline("Reviewer", original, modified, simplify_move_markup=True) # ValueErrorThey raise rather than being ignored on purpose. Two of them are still accepted by the
underlying CLI, which warns and does nothing; --engine is rejected outright. Silently dropping
engine="wmlcomparer" would have handed you DocxDiff output while you believed you had selected
something else — a wrong answer, not a breaking change. Unknown keyword arguments are rejected on
the same reasoning: a typo used to vanish in silence.
This repository doubles as a GitHub Action, so a repo that versions .docx files can get a
redline of every Word document a pull request changes — as a reviewable workflow artifact —
without anyone opening Word (#12).
name: DOCX redlines
on: pull_request
permissions:
contents: read
jobs:
redline:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # both sides of the comparison must be fetchable
- uses: JSv4/Python-Redlines@mainThat's the whole workflow. For every .docx file the pull request modifies (or renames), the
action extracts the base and head versions from git, runs the redline engine, and:
- writes
<output-dir>/<path>/<name>.redline.docxwith native Word tracked changes, - uploads the output directory as a workflow artifact,
- posts a job-summary table (file, revision count, output paths),
- when HTML previews are on, also writes a browser-viewable
.redline.htmlper file, rendered with the DocxodusDocx2Htmltool's--track-changesmode so insertions/deletions/moves show asins/delmarkup.
You can also compare an explicit pair of files instead of auto-detecting:
- uses: JSv4/Python-Redlines@main
with:
original: docs/contract-v1.docx
modified: docs/contract-v2.docx
author: Legal Review| Input | Default | Purpose |
|---|---|---|
original / modified |
— | Explicit-pair mode: compare these two files instead of auto-detecting. |
files |
**/*.docx |
Newline-separated git glob pattern(s) selecting which changed files to redline. |
base-ref / head-ref |
event-derived | Commits to compare. Defaults: PR base (merge-base) → head on pull_request, before → after on push, else HEAD~1 → HEAD. |
author |
python-redlines |
Author recorded on the tracked changes. |
engine |
docxodus |
docxodus or xmlpowertools. |
comparison |
— | Removed in 1.0.0. Setting it fails the run; there is no longer an algorithm to select. |
detect-moves |
false |
Move detection (docxodus engine only). |
output-dir |
redlines |
Where outputs are written (mirrors the source tree). |
html-preview |
auto |
auto (render when the Docx2Html tool supports --track-changes, else warn and skip), true (require), false (skip — no .NET needed). |
summary |
true |
Write the job-summary table. |
upload-artifact / artifact-name |
true / docx-redlines |
Artifact upload controls. |
package-version |
latest | pip pin for python-redlines, e.g. ==1.0.0. |
docx2html-version |
latest | NuGet pin for the Docx2Html preview tool. |
Outputs: count (redlines generated), any-changes, redlines (a JSON array of
{path, previous_path, status, revisions, redline, html, error} records), and artifact-url.
Notes:
- Added and deleted
.docxfiles are listed in the summary but not redlined — a redline needs both a base and a head version. Pure renames report zero revisions without invoking the engine. - HTML previews need the
Docx2Htmldotnet tool with--track-changessupport (Docxodus ≥ 7.1.0). That is now on NuGet, so the defaultautomode renders previews rather than skipping them.autostill degrades to a warning-and-skip when the tool is missing or whendocx2html-versionpins it below 7.1.0; usehtml-preview: trueto require a preview and fail the run if one cannot be produced. - The action installs python-redlines from PyPI with prebuilt engine binaries — it does
not build anything from the repository, so runs are fast on
ubuntu-latestrunners.
Python-Redlines ships two engine classes, each wrapping one algorithm.
| # | Choice | How to select it | Algorithm | Status |
|---|---|---|---|---|
| 1 | Docxodus | DocxodusEngine() |
DocxDiff — structure-aware IR diff |
✅ Default. Actively maintained |
| 2 | Open-XML-PowerTools | XmlPowerToolsEngine() |
Original WmlComparer |
If you are unsure, use choice 1. It is the default and requires no arguments.
Until 1.0.0 there was a third choice — Docxodus running a modernized WmlComparer, selected with
engine="wmlcomparer". Docxodus v11.0.0 deleted it, so that choice is gone and the keyword
argument raises ValueError.
Docxodus is an actively-maintained .NET 10.0 document
toolchain. Its DocxDiff algorithm models the document as an intermediate representation rather
than a stream of runs, which lets it attribute a change to the exact paragraph, cell, row, or
section it touched:
- Structure-aware — tracks table cell and row properties, section properties, and header and footer content that a run-stream diff reports coarsely or not at all
- Native move detection — identifies content that moved rather than deleting and re-inserting it
- Format change detection — detects changes to bold, italic, font size, and other run properties
- Actively maintained — regular bug fixes and new features
from python_redlines import DocxodusEngine
engine = DocxodusEngine()
redline_bytes, stdout, stderr = engine.run_redline("AuthorName", original_bytes, modified_bytes)Wraps the original Open-XML-PowerTools
WmlComparer. Instantiating it emits a DeprecationWarning.
from python_redlines import XmlPowerToolsEngine
engine = XmlPowerToolsEngine() # DeprecationWarning
redline_bytes, stdout, stderr = engine.run_redline("AuthorName", original_bytes, modified_bytes)Note: Open-XML-PowerTools was archived by Microsoft and is no longer maintained. This class and its
python-redlines-ooxmlpowertoolswheel still ship, and still work, so that anyone who needs the original algorithm's output has somewhere to stand. It will be removed in a future major release — move toDocxodusEnginewhen you can.
Both share the same call signature — run_redline(author, original, modified) returning
(bytes, stdout, stderr). They differ in the class you instantiate, which keyword arguments they
accept, and their stdout format (see Stdout Differences below).
The comparison engines are compiled .NET binaries, but they are prebuilt and embedded
in the published wheels — you do not need the .NET SDK (or any local compilation) to
install or use python-redlines.
Each engine ships in its own optional companion package. Install the engine(s) you need as extras:
pip install python-redlines[docxodus] # Docxodus engine (recommended)
pip install python-redlines[ooxmlpowertools] # Open-XML-PowerTools engine (deprecated)
pip install python-redlines[all] # both engine packages
Prebuilt wheels are available for Linux, macOS, and Windows (x64 and arm64); pip
selects the wheel matching your platform automatically. Instantiating an engine whose
companion package is not installed raises EngineNotInstalledError telling you which
extra to install.
See the Quick Start above for a minimal example, or the quickstart guide for a step-by-step walkthrough.
DocxodusEngine supports fine-grained control over the comparison via keyword arguments to
run_redline(). XmlPowerToolsEngine accepts none of them.
from python_redlines import DocxodusEngine
engine = DocxodusEngine()
redline_bytes, stdout, stderr = engine.run_redline(
"Reviewer", original, modified,
detect_moves=True,
case_insensitive=True,
)| Setting | Type | Default | Description |
|---|---|---|---|
case_insensitive |
bool | False |
Ignore case differences |
detect_moves |
bool | False |
Enable move detection |
move_similarity_threshold |
float | 0.8 |
Jaccard threshold for move matching (0.0–1.0) |
move_minimum_word_count |
int | 3 |
Minimum words for move detection |
detect_format_changes |
bool | True |
Detect block-level formatting changes |
conflate_spaces |
bool | True |
Treat breaking/non-breaking spaces the same |
date_time |
str | now | Custom ISO 8601 timestamp for revisions |
Anything else raises ValueError, including a misspelled setting name. Passing a setting to
XmlPowerToolsEngine is still silently ignored — it has never accepted any.
| Setting | Why it is gone |
|---|---|
engine |
Selected between wmlcomparer and docxdiff. Docxodus v11.0.0 deleted WmlComparer, so there is nothing to select. |
detail_threshold |
Tuned WmlComparer's LCS granularity. DocxDiff's granularity is structural and has no equivalent knob. |
simplify_move_markup |
Worked around WmlComparer's move markup. DocxDiff renders moves natively. |
All three raise ValueError naming the removal and what to do instead. The check is on the
keyword being present, whatever its value: simplify_move_markup=False raises too.
The 0.3.0-era warning that detect_moves=True needed simplify_move_markup=True to avoid Word's
"unreadable content" dialog applied to WmlComparer's move markup. It does not apply here —
DocxDiff emits move markup natively and has no such mitigation, because it needs none.
If you called run_redline with no keyword arguments, nothing in your code changes. Your
output will change, because the algorithm changed.
- Remove
engine=,detail_threshold=andsimplify_move_markup=. They raiseValueError. If you were passingengine="docxdiff", delete the argument — you now get it by default. - Re-baseline anything that asserts on revision counts. On this project's own fixtures the
count moved from 9 (
wmlcomparer) and 11 (docxdiffin 0.3.0) to 10. Upstream v11 and v12 changed region arrangement, surplus table cells and section defaults, so the 0.3.0docxdiffcount is not the 1.0.0 count either. - Check documents that already carry tracked changes. Docxodus v11 dropped
PreserveInputRevisionsfromDocxCompare.Compare's front door to match Word's own Compare behaviour. Revision-bearing inputs therefore produce different output than in 0.3.0. This project's fixtures carry no input revisions and will not warn you; diff a representative document if this is your workload. - Move off
XmlPowerToolsEnginewhen you can — it now warns, and will be removed in a future major release. - In the GitHub Action, remove the
comparison:input. It fails the run.
Pin python-redlines==0.3.0 if you need the old wmlcomparer output while you migrate.
Both engine classes follow the same pattern: a Python wrapper class invokes a self-contained C# binary via subprocess.
The repository is a monorepo of three separately-published packages:
| Package | PyPI name | Contents |
|---|---|---|
packages/core |
python-redlines |
Pure-Python wrapper; no binaries |
packages/ooxmlpowertools |
python-redlines-ooxmlpowertools |
Open-XML-PowerTools engine binary |
packages/docxodus |
python-redlines-docxodus |
Docxodus engine binary |
The core package's [docxodus] / [ooxmlpowertools] / [all] extras pull in the
binary packages. Each binary package is published as per-platform wheels (Linux,
macOS, Windows × x64/arm64), each embedding one prebuilt, self-contained .NET binary.
python-redlines/
│
├── csproj/ # XmlPowerTools C# source
├── docxodus/ # Docxodus git submodule (tools/redline/)
│
├── packages/
│ ├── core/ # -> python-redlines
│ │ └── src/python_redlines/ # engines.py, __init__.py, __about__.py
│ ├── ooxmlpowertools/ # -> python-redlines-ooxmlpowertools
│ │ ├── hatch_build.py # stamps the wheel platform tag
│ │ └── src/python_redlines_ooxmlpowertools/_binaries/
│ └── docxodus/ # -> python-redlines-docxodus
│ ├── hatch_build.py
│ └── src/python_redlines_docxodus/_binaries/
│
├── tests/ # integration + contract tests (run from root)
├── build_differ.py # compiles engines into each package's _binaries/
└── pyproject.toml # shared pytest/coverage config only
At runtime the wrapper finds its companion binary package via importlib.resources,
extracts the platform archive once into the user cache directory, and runs it.
The engines produce slightly different stdout messages:
| Engine | Example stdout |
|---|---|
XmlPowerToolsEngine |
Revisions found: 9 |
DocxodusEngine |
Redline complete: 10 revision(s) found |
The counts differ because the algorithms differ, not because either is wrong.
Looking for a .docx comparison tool and weighing it against a paid API? Short
version: Python-Redlines is a free, MIT-licensed, self-hosted library — your documents
never leave your process, there's no per-comparison fee, and it works fully offline.
The full write-up, including feature-by-feature tables, lives in
docs/alternatives.md:
Draftable's document comparison API is metered per comparison and requires sending your documents to a third-party cloud service. Python-Redlines runs in-process with no per-call cost, no network dependency, and full source availability (MIT license). See the detailed comparison.
Cloudmersive's document comparison endpoint is one part of a broader, metered, closed-source API. Python-Redlines has no API keys, no quotas, and keeps document bytes local to your own infrastructure. See the detailed comparison.
Driving a headless copy of MS Word via COM/Interop to generate tracked changes
violates Microsoft's Terms of Service for unattended server-side use and is
operationally fragile under concurrent load. Python-Redlines produces the same native
Word tracked-changes .docx output via a lightweight, self-contained comparison
engine — no Word installation, no TOS risk. See
the full breakdown.
- Python 3.9+
- .NET 10.0 SDK (only for building the engine binaries locally). Install with
apt install dotnet-sdk-10.0, orcurl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 10.0. A .NET 8 SDK can no longer build the Docxodus engine, which targetsnet10.0.
# Clone with submodules
git clone --recurse-submodules https://github.com/JSv4/Python-Redlines
cd Python-Redlines
# If you already cloned without submodules
git submodule update --init --recursive
# Build the engine binaries for your platform (RIDs: linux-x64, win-x64, osx-arm64, ...)
python build_differ.py linux-x64
# Install all three packages editable
pip install -e packages/core -e packages/ooxmlpowertools -e packages/docxodus pytest# Run tests (from the repo root)
python -m pytest tests/
# Run a single test
python -m pytest tests/test_openxml_differ.py::test_run_redlines_with_real_files
# Build engine binaries for one or more platforms
python build_differ.py linux-x64 win-x64
python build_differ.py --all
# Build a package wheel
python -m build packages/coreIf you want to contribute to the library or want to dive into some of the C# packaging architecture, go to our developer guide.
- Contributing: Contributions to the project should follow the established coding and documentation standards.
- Issues and Support: For issues, feature requests, or support, please use the project's issue tracker on GitHub.
MIT