Skip to main content

yaxmldiff – Yet Another XML Diff Library

[ GitHub: latk/yaxmldiff.py | PyPI: yaxmldiff ]

This library checks if two XML documents seem semantically equivalent. If not, it produces something similar to a unified diff.

Example:

>>> from yaxmldiff import compare_xml
>>> print(compare_xml("<same/>", "  <same /> <!--ignored-->"))
None
>>> print(compare_xml("<doc><a id='a'/></doc>", "<doc><a name='a'/></doc>"))
  <doc>
    <a
-     id="a"
+     name="a"
    />
  </doc>

Installation

Yaxmldiff is available on PyPI: https://pypi.org/project/yaxmldiff

If you want to use yaxmldiff as a Python library, add it to your project dependencies. How to do this depends on your project manager. Examples:

  • pip install yaxmldiff
  • uv add yaxmldiff
  • poetry add yaxmldiff

Installing the library will also install the command line interface.

If you only want to use the CLI, you can install into a dedicated venv using tools such as uv or pipx:

  • uv tool install yaxmldiff
  • pipx install yaxmldiff

If you want to try out the CLI without installing permanently, you can use:

  • uvx yaxmldiff
  • pipx run yaxmldiff

You can also preview a bleeding-edge version from GitHub:

  • uvx git+https://github.com/latk/yaxmldiff.py
  • pipx run --spec git+https://github.com/latk/yaxmldiff.py yaxmldiff

compare_xml()

Compare two XML documents.

If the documents are given as strings, they are parsed first. Alternatively, the documents can be given as an lxml.etree object.

Args:

  • left: an input document
  • right: an input document
  • context: how many lines of context to preserve around each change
  • comments: whether comments and processing instructions will be diffed as well (default: true)

Returns: None if both are equal, a diff otherwise.

Signature:

def compare_xml(
    left: str | Element,
    right: str | Element,
    *,
    context: int = 3,
    comments: bool = True,
) -> str | None:

Command line interface

Yaxmldiff can also be used as a command line tool.

Installation: The CLI is installed as part of the yaxmldiff package, see instructions above.

Usage: yaxmldiff LEFT RIGHT [--html, --no-html] [-U N] [--comments, --no-comments] [--exit-code, --no-exit-code] [--quiet, --no-quiet] [-h] [-v] [--completion COMPLETION]

Compare two XML files via a structural diff.

The output is similar to an unified diff (like the one used by Git), but the diff is performed structurally: element by element, not line by line. Whitespace is generally ignored.

Input Options:

  • LEFT

    A file to compare.

  • RIGHT

    A file to compare.

  • [--html, --no-html]

    Parse the files as HTML.

Options:

  • [-U, --context, --unified N]

    How many lines of context to show around each change.

    The output will always use the "unified diff" format, never the "context" format. Where content is elided, the output cannot use the @@ ... @@ markers due to the structural nature of the diff, and will instead insert a placeholder like ... skipped 12 lines.

  • [--comments, --no-comments]

    Whether comments and processing instructions will be diffed as well.

  • [--exit-code, --no-exit-code]

    Exit with code 1 if files differ, just like the standard diff tool.

  • [--quiet, --no-quiet]

    Don't print the diff, only check if the inputs differ. Implies --exit-code.

Help:

  • [-h, --help]

    Show this message and exit.

  • [-v, --version]

    Show the version and exit.

  • [--completion COMPLETION]

    Use --completion generate to print shell-specific completion source.

Exit code: exits with 0 on success, or 2 if there was a problem. If --exit-code or --quiet were enabled, exit with code 1 if the files differ.

More info at the yaxmldiff website: https://github.com/latk/yaxmldiff.py

Examples

Example: equal documents

>>> print(compare_xml("<a/>", "<a/>"))
None

Example: different tag

>>> print(compare_xml("<a/>", "<b x='2'/>"))
- <a/>
+ <b .../>

Example: changed text

>>> print(compare_xml("<root><a/>foo</root>", "<root><a/>bar</root>"))
  <root>
    <a/>
-   foo
+   bar
  </root>

Example: nested changed text, collapses other nodes

>>> print(
...     compare_xml(
...         "<root><uninteresting a='b'>foo</uninteresting><scope>a</scope></root>",
...         "<root><uninteresting a='b'>foo</uninteresting><scope>b</scope></root>",
...     )
... )
  <root>
    <uninteresting ...>...</uninteresting>
    <scope>
-     a
+     b
    </scope>
  </root>

Example: inserted and removed nodes

>>> print(compare_xml("<r><a/><c/></r>", "<r><a/><b/><c/></r>"))
  <r>
    <a/>
+   <b/>
    <c/>
  </r>
>>> print(compare_xml("<r><a/><b/><c/></r>", "<r><a/><c/></r>"))
  <r>
    <a/>
-   <b/>
    <c/>
  </r>

Example: changed attributes

>>> print(
...     compare_xml(
...         "<a onlya='1' both='2' changed='3'/>",
...         "<a onlyb='1' both='2' changed='4'/>",
...     )
... )
  <a both="2"
-   changed="3"
-   onlya="1"
+   changed="4"
+   onlyb="1"
  />

Example: changed attrs collapse content

>>> print(compare_xml("<a>content</a>", "<a attribute='value'>content</a>"))
  <a
+   attribute="value"
  >
    ...
  </a>

Example: collapse common context

>>> print(
...     compare_xml(
...         "<root><a/><b/><c/><d/><e/><f/><changed-left/></root>",
...         "<root><a/><b/><c/><d/><e/><f/><changed-right/></root>",
...     )
... )
  <root>
    ... skipped 3 lines
    <d/>
    <e/>
    <f/>
-   <changed-left/>
+   <changed-right/>
  </root>

Example: can hande encoding declarations

>>> print(
...     compare_xml(
...         "<?xml version='1.0' encoding='UTF-8'?><a/>",
...         "<a/>",
...     )
... )
None

Example: comparison ignores surrounding space and newlines

>>> print(compare_xml("<a>b<c/></a>", "\n <a> \n b \n <c \n/> \n </a> \n "))
None

Example: pre-parse documents

>>> import lxml.etree
>>> print(compare_xml(lxml.etree.XML('<a parsed="yes"/>'), "<a parsed='no'/>"))
  <a
-   parsed="yes"
+   parsed="no"
  />

Example: can handle XML namespaces

>>> print(
...     compare_xml(
...         '<html:html xmlns:html="http://www.w3.org/1999/xhtml"><html:body>explicit namespaces</html:body></html:html>',
...         '<html xmlns="http://www.w3.org/1999/xhtml"><body>implicit namespaces</body></html>',
...     )
... )
  <html ...>
    <body ...>
-     explicit namespaces
+     implicit namespaces
    </body>
  </html>
>>> print(
...     compare_xml(
...         '<html xmlns="http://www.w3.org/1999/xhtml"><body>implicit namespaces</body></html>',
...         "<html><body>no namespaces</body></html>",
...     )
... )
- <html xmlns="http://www.w3.org/1999/xhtml">...</html>
+ <html>...</html>

Example: can optionally handle comments

>>> print(compare_xml("<a><!-- foo --></a>", "<a><!-- bar --></a>"))
  <a>
    <!--
-     foo
+     bar
    -->
  </a>
>>> print(compare_xml("<a><!-- same --><x/></a>", "<a><!-- same --><y/></a>"))
  <a>
    <!-- same -->
-   <x/>
+   <y/>
  </a>
>>> print(compare_xml("<a><!-- foo --></a>", "<a><!-- bar --></a>", comments=False))
None
>>> print(
...     compare_xml("<a><!-- same --><x/></a>", "<a><!-- same --><y/></a>", comments=False)
... )
  <a>
-   <x/>
+   <y/>
  </a>

Example: can handle processing instructions

>>> print(compare_xml("<a><?php echo 123; ?></a>", '<a><?php echo "abc"; ?></a>'))
  <a>
    <?php
-     echo 123;
+     echo "abc";
    ?>
  </a>
>>> print(compare_xml("<a><?math 3*3 ?></a>", '<a><?php echo "abc"; ?></a>'))
  <a>
-   <?math 3*3?>
+   <?php echo "abc";?>
  </a>

Example: can diff nodes of different types

>>> print(compare_xml("<a>text<!-- comment --></a>", "<a><element/><?pi?></a>"))
  <a>
-   text
+   <element/>
-   <!-- comment -->
+   <?pi ?>
  </a>

Related software

There are tons of XML diffing tools.

Most closely related is lxml.doctestcompare which is specifically intended for Python doctests. It has test-oriented features such as ignoring subtrees with an <any> tag or content with an ... ellipsis. In contrast, yaxmldiff will compare two documents without further transformations. Another big difference is in the output. Whereas lxml will add inline annotations, yaxmldiff tries to emulate a unified diff, and will collapse uninteresting parts of the document.

The xmldiff tool (PyPI: xmldiff GitHub: Shoobx/xmldiff) is a much better diff tool than yaxmldiff. It can be used both as a Python library and as a CLI. However, it produces an XML patch: XPath-based instructions on what to move, add, or delete. This output is not particularly human-readable, as there's no context. An alternative XML formatter renders the diff in XML form, but that will show the full document, plus diff instructions nodes from a diff XML-namespace.

xmldiff vs yaxmldiff output comparison
$ uvx xmldiff --version
xmldiff 3.0

$ uvx xmldiff a.xml b.xml
[update-text, /foo[1], "right", "left"]

$ uvx xmldiff a.xml b.xml --pretty-print --formatter xml
<foo xmlns:diff="http://namespaces.shoobx.com/diff"><diff:delete>lef</diff:delete><diff:insert>righ</diff:insert>t</foo>

$ yaxmldiff a.xml b.xml
  <foo>
-   left
+   right
  </foo>

The difftastic tool has really good support for structural diffs across many languages. However, it is purely a command line tool. It cannot be used from Python as a library. The output also relies on color, unlike the unified diff format.

difftastic vs yaxmldiff output comparison
$ difftastic --version
Difftastic 0.69.0

Revision:  90a0f1b6a 2026-04-29
Toolchain: 1.85.0
System:    linux x86_64 Snap

$ difftastic a.xml b.xml --display=inline
b.xml --- XML
1    <foo>left</foo>
   1 <foo>right</foo>

$ yaxmldiff a.xml b.xml
  <foo>
-   left
+   right
  </foo>

Recommendations:

If you're looking for a Python-based XML diffing library or CLI tool:

  • pick the lxml doctest features if you want to match XML output in a doctest
  • pick xmldiff if you want to generate machine-readable diffs
  • pick yaxmldiff if you want to generate plaintext human-readable diffs

If you're looking for a command-line tool:

  • pick difftastic if you want human-readable output with best-in-class structural diffing, and can rely on colored output
  • pick yaxmldiff if you want human-readable output that works as plaintext

Contributing

Use uv for virtualenv management. After installing uv, run uv sync --all-extras --dev to install dependencies.

Common development tasks are managed via the just tasks runner. Install it via your package manager. If in doubt, use pipx install rust-just. Once installed, run just or just qa for a complete QA pipeline with linters+typechecking+tests. Run just -l to get a list of all recipes.

License

Copyright 2021-2026 Lukas Atkinson

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Changelog

0.3.0 - 2026-08-08

  • minimum Python version is 3.10
  • minimum lxml version is 5
  • support XML documents with comments or processing instructions (default: include comments and PIs in the diff)
  • improved support for XML namespaces
  • collapse repeated context lines at the same level (default: show 3 lines around each change)
  • smarter detection of inserted/deleted nodes
  • new command line interface (usage: yaxmldiff left.xml right.xml)
  • (internal) refactor diffing logic to support the new features
  • (internal) packaging updates

0.2.0 – 2024-09-29

  • minimum Python version is 3.8
  • (internal) packaging modernization

0.1.0 - 2021-06-13

  • initial release

Metadata

Release files for yaxmldiff 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for yaxmldiff 0.3.0
File Size Uploaded Provenance
yaxmldiff-0.3.0.tar.gz 97.6 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for yaxmldiff 0.3.0
File Interpreter ABI Platform
yaxmldiff-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 114.4 kB

Release files / yaxmldiff-0.3.0.tar.gz

Download URL yaxmldiff-0.3.0.tar.gz
Size 97.6 kB
Tags Source
SHA-256 checksum
How to use checksums
1acde7690444be323c2eefa272360405b0719f9b637ec1b2ea65cc966deb9bb7
BLAKE2b-256 checksum
How to use checksums
b5013a2a92ea33e37e5b42165c7aae63768b5d8c4979f3f4902473833badc287
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 8, 2026.

Transparency log

Release files / yaxmldiff-0.3.0-py3-none-any.whl

Download URL yaxmldiff-0.3.0-py3-none-any.whl
Size 16.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
565d3fd649cc8b6814b904c44d5c3d18394d2483cb115c82b07fb5562033a6a2
BLAKE2b-256 checksum
How to use checksums
4f1e76745f9437a368186555fdcbcf6658ee1ff80d6d5795d419cab493520059
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Aug 8, 2026.

Transparency log

Security for yaxmldiff 0.3.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.

Select from 2 files

Attestation for yaxmldiff-0.3.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 Aug 8, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page