Python Track Changes Library for Word Documents
Read and write track changes in Word documents (.docx) with Python. docx-revisions is a Python library that extends python-docx to support Microsoft Word track changes and document revisions — OOXML revision markup (insertions and deletions) — so you can programmatically accept text, list revisions with author and date, and apply insertions, deletions, or replacements with full revision metadata.
Installation
pip install docx-revisions
uv add docx-revisions
Quick Start
Read tracked changes
from docx_revisions import RevisionDocument
rdoc = RevisionDocument("tracked_changes.docx")
for para in rdoc.paragraphs:
if para.has_track_changes:
for change in para.track_changes:
print(f"{type(change).__name__}: '{change.text}' by {change.author}")
print(f"Accepted: {para.accepted_text}")
print(f"Original: {para.original_text}")
Accept or reject all changes
from docx_revisions import RevisionDocument
rdoc = RevisionDocument("tracked_changes.docx")
rdoc.accept_all() # or rdoc.reject_all()
rdoc.save("clean.docx")
Find and replace with tracking
from docx_revisions import RevisionDocument
rdoc = RevisionDocument("contract.docx")
count = rdoc.find_and_replace_tracked("Acme Corp", "NewCo Inc", author="Legal")
print(f"Replaced {count} occurrences")
rdoc.save("contract_revised.docx")
Add tracked insertions and deletions
from docx_revisions import RevisionParagraph
rp = RevisionParagraph.from_paragraph(paragraph)
rp.add_tracked_insertion("new text", author="Editor")
rp.add_tracked_deletion(start=5, end=10, author="Editor")
API reference
docx_revisions
docx-revisions: Track changes support for python-docx.
RevisionDocument
Entry point for reading and writing tracked changes in a docx file.
Example
from docx_revisions import RevisionDocument
rdoc = RevisionDocument("contract.docx")
for para in rdoc.paragraphs:
if para.has_track_changes:
print(para.accepted_text)
rdoc.accept_all()
rdoc.save("contract_clean.docx")
Source code in docx_revisions/document.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |
document
property
The underlying python-docx Document object.
paragraphs
property
All body paragraphs as RevisionParagraph objects.
track_changes
property
All tracked changes across the entire document body.
__init__(path_or_doc=None)
Open a docx file or wrap an existing Document.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path_or_doc
|
str | Path | IO[bytes] | Document | None
|
A file path, file-like object, or an existing
|
None
|
Source code in docx_revisions/document.py
37 38 39 40 41 42 43 44 45 46 47 48 | |
accept_all()
Accept every tracked change in the document.
Insertions are kept (wrapper removed), deletions are removed entirely.
Source code in docx_revisions/document.py
68 69 70 71 72 73 74 75 | |
reject_all()
Reject every tracked change in the document.
Insertions are removed entirely, deletions are kept (wrapper removed,
w:delText converted back to w:t).
Source code in docx_revisions/document.py
77 78 79 80 81 82 83 84 85 | |
find_and_replace_tracked(search_text, replace_text, author='', comment=None)
Find and replace across the whole document with track changes.
Searches all paragraphs in the document body and tables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
search_text
|
str
|
Text to find. |
required |
replace_text
|
str
|
Replacement text. |
required |
author
|
str
|
Author name for the revisions. |
''
|
comment
|
str | None
|
Optional comment text (requires python-docx comment support). |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Total number of replacements made. |
Example
rdoc = RevisionDocument("doc.docx")
count = rdoc.find_and_replace_tracked(
"Acme Corp", "NewCo Inc", author="Legal"
)
print(f"Replaced {count} occurrences")
rdoc.save("doc_revised.docx")
Source code in docx_revisions/document.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | |
save(path)
Save the document to path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Destination file path. |
required |
Source code in docx_revisions/document.py
128 129 130 131 132 133 134 | |
RevisionParagraph
Bases: Paragraph
A Paragraph subclass that adds track-change support.
Create from an existing Paragraph with from_paragraph() — the
two objects share the same underlying XML element so mutations via either
reference are visible to both.
Example
from docx import Document
from docx_revisions import RevisionParagraph
doc = Document("example.docx")
for para in doc.paragraphs:
rp = RevisionParagraph.from_paragraph(para)
if rp.has_track_changes:
print(f"Insertions: {len(rp.insertions)}")
print(f"Deletions: {len(rp.deletions)}")
Source code in docx_revisions/paragraph.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 | |
has_track_changes
property
True if this paragraph contains any w:ins or w:del children.
insertions
property
All tracked insertions in this paragraph, in document order.
deletions
property
All tracked deletions in this paragraph, in document order.
track_changes
property
All tracked changes (insertions and deletions) in document order.
accepted_text
property
Text of this paragraph with all changes accepted.
Insertions are kept, deletions are removed.
original_text
property
Text of this paragraph with all changes rejected.
Deletions are kept, insertions are removed.
from_paragraph(para)
classmethod
Create a RevisionParagraph that shares para's XML element.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
para
|
Paragraph
|
An existing |
required |
Returns:
| Type | Description |
|---|---|
RevisionParagraph
|
A |
Source code in docx_revisions/paragraph.py
53 54 55 56 57 58 59 60 61 62 63 | |
iter_inner_content(include_revisions=False)
Generate runs, hyperlinks, and optionally revisions in document order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
include_revisions
|
bool
|
If True, also yields |
False
|
Yields:
| Type | Description |
|---|---|
Run | Hyperlink | TrackedInsertion | TrackedDeletion
|
|
Run | Hyperlink | TrackedInsertion | TrackedDeletion
|
objects in document order. |
Source code in docx_revisions/paragraph.py
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |
add_tracked_insertion(text=None, style=None, author='', revision_id=None)
Append a tracked insertion containing a run with the specified text.
The run is wrapped in a w:ins element, marking it as inserted
content when track changes is enabled.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str | None
|
Text to add to the run. |
None
|
style
|
str | CharacterStyle | None
|
Character style to apply to the run. |
None
|
author
|
str
|
Author name for the revision. Defaults to empty string. |
''
|
revision_id
|
int | None
|
Unique ID for this revision. Auto-generated if not provided. |
None
|
Returns:
| Type | Description |
|---|---|
TrackedInsertion
|
A |
Example
rp = RevisionParagraph.from_paragraph(paragraph)
tracked = rp.add_tracked_insertion("new text", author="Editor")
print(tracked.text)
Source code in docx_revisions/paragraph.py
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
add_tracked_deletion(start, end, author='', revision_id=None)
Wrap existing text at [start, end) in a w:del element.
The text remains in the document but is marked as deleted. The
corresponding w:t elements are converted to w:delText.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
int
|
Starting character offset (0-based, inclusive). |
required |
end
|
int
|
Ending character offset (0-based, exclusive). |
required |
author
|
str
|
Author name for the revision. |
''
|
revision_id
|
int | None
|
Unique ID for this revision. Auto-generated if not provided. |
None
|
Returns:
| Type | Description |
|---|---|
TrackedDeletion
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If offsets are invalid. |
Source code in docx_revisions/paragraph.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | |
replace_tracked(search_text, replace_text, author='', comment=None)
Replace all occurrences of search_text with replace_text using track changes.
Each replacement creates a tracked deletion of search_text and a tracked insertion of replace_text. Matches text across run boundaries (handles OOXML run splitting).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
search_text
|
str
|
Text to find and replace. |
required |
replace_text
|
str
|
Text to insert in place of search_text. |
required |
author
|
str
|
Author name for the revision. |
''
|
comment
|
str | None
|
Optional comment text (requires python-docx comment support). |
None
|
Returns:
| Type | Description |
|---|---|
int
|
The number of replacements made. |
Example
rp = RevisionParagraph.from_paragraph(paragraph)
count = rp.replace_tracked("old", "new", author="Editor")
Source code in docx_revisions/paragraph.py
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
replace_tracked_at(start, end, replace_text, author='', comment=None)
Replace text at character offsets [start, end) using track changes.
Creates a tracked deletion of the text at positions [start, end)
and a tracked insertion of replace_text at that position. The
offsets are relative to paragraph.text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
int
|
Starting character offset (0-based, inclusive). |
required |
end
|
int
|
Ending character offset (0-based, exclusive). |
required |
replace_text
|
str
|
Text to insert in place of the deleted text. |
required |
author
|
str
|
Author name for the revision. |
''
|
comment
|
str | None
|
Optional comment text (requires python-docx comment support). |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If start or end are out of bounds or start >= end. |
Source code in docx_revisions/paragraph.py
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
TrackedChange
Bases: Parented
Base class for tracked change proxy objects.
Provides common functionality for both insertions and deletions.
Source code in docx_revisions/revision.py
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
author
property
writable
The author who made this change.
date
property
writable
The date/time when this change was made, or None if not recorded.
revision_id
property
writable
The unique identifier for this revision.
is_block_level
property
True if this change contains block-level content (paragraphs/tables).
is_run_level
property
True if this change contains run-level content.
paragraphs
property
List of paragraphs in this change (for block-level changes).
runs
property
List of runs in this change (for run-level changes).
iter_inner_content()
Generate Paragraph or Table objects for block-level content.
Source code in docx_revisions/revision.py
77 78 79 80 81 82 83 84 85 86 87 | |
iter_runs()
Generate Run objects for run-level content.
Source code in docx_revisions/revision.py
89 90 91 92 93 94 | |
accept()
Accept this tracked change.
For an insertion, this removes the revision wrapper, keeping the content. For a deletion, this removes both the wrapper and the content.
Source code in docx_revisions/revision.py
116 117 118 119 120 121 122 | |
reject()
Reject this tracked change.
For an insertion, this removes both the wrapper and the content. For a deletion, this removes the revision wrapper, keeping the content.
Source code in docx_revisions/revision.py
124 125 126 127 128 129 130 | |
TrackedDeletion
Bases: TrackedChange
Proxy object wrapping a w:del element.
Represents content that was deleted while track changes was enabled. The deleted content is still present in the document but marked as deleted.
Source code in docx_revisions/revision.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
text
property
The text content of this deletion.
For block-level deletions, returns concatenated text of all paragraphs.
For run-level deletions, concatenates text from w:delText elements
found via xpath, since Run.text only reads w:t.
accept()
Accept this deletion, removing both the content and the revision wrapper.
Source code in docx_revisions/revision.py
196 197 198 199 200 | |
reject()
Reject this deletion, keeping the content but removing the revision wrapper.
Also converts w:delText elements back to w:t so the text
becomes visible again.
Source code in docx_revisions/revision.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
TrackedInsertion
Bases: TrackedChange
Proxy object wrapping a w:ins element.
Represents content that was inserted while track changes was enabled. The inserted content can be paragraphs, tables, or runs depending on context.
Source code in docx_revisions/revision.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
text
property
The text content of this insertion.
For block-level insertions, returns concatenated text of all paragraphs. For run-level insertions, returns concatenated text of all runs.
accept()
Accept this insertion, keeping the content but removing the revision wrapper.
Source code in docx_revisions/revision.py
152 153 154 155 156 157 158 159 160 161 162 | |
reject()
Reject this insertion, removing both the content and the revision wrapper.
Source code in docx_revisions/revision.py
164 165 166 167 168 | |
RevisionRun
Bases: Run
A Run subclass that adds track-change support.
Create from an existing Run with from_run() — the two objects
share the same underlying XML element.
Example
from docx_revisions import RevisionRun
run = paragraph.runs[0]
rr = RevisionRun.from_run(run)
tracked = rr.delete_tracked(author="Editor")
Source code in docx_revisions/run.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | |
from_run(run)
classmethod
Create a RevisionRun that shares run's XML element.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
run
|
Run
|
An existing |
required |
Returns:
| Type | Description |
|---|---|
RevisionRun
|
A |
Source code in docx_revisions/run.py
35 36 37 38 39 40 41 42 43 44 45 | |
delete_tracked(author='', revision_id=None)
Mark this run as deleted with track changes.
Instead of removing the run, it is wrapped in a w:del element to
mark it as deleted content. The run remains in the document but is
displayed as deleted text (e.g., with strikethrough). The w:t
elements are converted to w:delText.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
author
|
str
|
Author name for the revision. |
''
|
revision_id
|
int | None
|
Unique ID for this revision. Auto-generated if not provided. |
None
|
Returns:
| Type | Description |
|---|---|
TrackedDeletion
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the run has no parent element. |
Source code in docx_revisions/run.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | |
replace_tracked_at(start, end, replace_text, author='')
Replace text at character offsets [start, end) using track changes.
Creates a tracked deletion of the text at positions [start, end)
and a tracked insertion of replace_text at that position.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
start
|
int
|
Starting character offset (0-based, inclusive). |
required |
end
|
int
|
Ending character offset (0-based, exclusive). |
required |
replace_text
|
str
|
Text to insert in place of the deleted text. |
required |
author
|
str
|
Author name for the revision. |
''
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If start or end are out of bounds or start >= end. |
Source code in docx_revisions/run.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |