Skip to content

How to Compare Two XML Files in Python

Learn how to accurately compare XML files in Python while handling whitespace, attribute ordering, and formatting differences that break simple text comparison.

Comparing XML documents requires special handling because structurally identical XML can have different text representations. Whitespace variations, attribute ordering, and formatting differences are all semantically meaningless but break simple text comparison.

This guide covers proper XML comparison techniques using both third-party libraries and Python's standard library.

Why Text Comparison Fails for XML

Standard string or line-by-line comparison produces false positives with XML:

xml1 = '<user id="1" name="Alice"/>'
xml2 = '<user name="Alice" id="1"/>'

print(xml1 == xml2)  # False (WRONG answer)

Config Files for the examples in the following sections

config1.xmlxml
<?xml version="1.0" encoding="UTF-8"?>
<config>
    <database>
        <host>localhost</host>
        <port>5432</port>
        <username>admin</username>
    </database>

    <server>
        <host>127.0.0.1</host>
        <port>8080</port>
    </server>

    <logging level="INFO"/>
</config>
config2.xmlxml
<?xml version="1.0" encoding="UTF-8"?>
<config>
    <database>
        <host>localhost</host>
        <port>5433</port>
        <username>admin</username>
    </database>

    <server>
        <host>127.0.0.1</host>
        <port>9090</port>
    </server>

    <logging level="DEBUG"/>

    <featureFlags>
        <feature name="betaMode" enabled="true"/>
    </featureFlags>
</config>

Semantic Comparison with xmldiff

The xmldiff library understands XML structure and reports meaningful differences:

pip install xmldiff
from xmldiff import main, formatting

def compare_xml_semantic(file1, file2):
    """Compare XML files and return structural differences."""
    diffs = main.diff_files(file1, file2)
    
    if not diffs:
        print("XML documents are semantically identical")
        return True
    
    print(f"Found {len(diffs)} differences:")
    for diff in diffs:
        print(f"  {diff}")
    
    return False

compare_xml_semantic('config1.xml', 'config2.xml')

Example Output:

Found 7 differences:
  UpdateAttrib(node='/config/logging[1]', name='level', value='DEBUG')
  InsertNode(target='/config[1]', tag='featureFlags', position=3)
  UpdateTextIn(node='/config/database/port[1]', text='5433')
  UpdateTextIn(node='/config/server/port[1]', text='9090')
  InsertNode(target='/config/featureFlags[1]', tag='feature', position=0)
  InsertAttrib(node='/config/featureFlags/feature[1]', name='enabled', value='true')
  InsertAttrib(node='/config/featureFlags/feature[1]', name='name', value='betaMode')

Getting Human-Readable Diff Output

from xmldiff import main, formatting

def get_xml_diff_report(file1, file2):
    """Generate formatted diff report."""
    formatter = formatting.DiffFormatter()
    diff = main.diff_files(
        file1, file2,
        formatter=formatter
    )
    return diff

get_xml_diff_report('config1.xml', 'config2.xml')
[update-attribute, /config/logging[1], level, "DEBUG"]
[insert, /config[1], featureFlags, 3]
[update-text, /config/database/port[1], "5433"]
[update-text, /config/server/port[1], "9090"]
[insert, /config/featureFlags[1], feature, 0]
[insert-attribute, /config/featureFlags/feature[1], enabled, "true"]
[insert-attribute, /config/featureFlags/feature[1], name, "betaMode"]
from xmldiff import main, formatting

def get_annotated_diff(file1, file2):
    """Get diff as annotated XML."""
    formatter = formatting.XMLFormatter(
        normalize=formatting.WS_BOTH
    )
    diff = main.diff_files(file1, file2, formatter=formatter)
    return diff

get_annotated_diff('config1.xml', 'config2.xml')
<config xmlns:diff="http://namespaces.shoobx.com/diff">
  <database>
    <host>localhost</host>
    <port>543<diff:delete>2</diff:delete><diff:insert>3</diff:insert></port>
    <username>admin</username>
  </database>
  <server>
    <host>127.0.0.1</host>
    <port><diff:delete>808</diff:delete><diff:insert>909</diff:insert>0</port>
  </server>
  <logging level="DEBUG" diff:update-attr="level:INFO"/>
  <featureFlags diff:insert="">
    <feature diff:insert="" enabled="true" diff:add-attr="enabled;name" name="betaMode"/>
  </featureFlags>
</config>

Canonicalization (Standard Library)

When external packages aren't available, use XML Canonicalization (C14N) to normalize documents before comparison:

import xml.etree.ElementTree as ET
import io
import os


def canonicalize_xml(filepath):
    """Convert XML to canonical form (Python 3.9 safe)."""

    if not os.path.exists(filepath):
        raise FileNotFoundError(f"{filepath} not found")

    output = io.StringIO()

    ET.canonicalize(
        from_file=filepath,
        out=output,
        with_comments=False
    )

    return output.getvalue().encode("utf-8")  # convert to bytes for safe compare


def compare_xml_canonical(file1, file2):
    canon1 = canonicalize_xml(file1)
    canon2 = canonicalize_xml(file2)

    if canon1 == canon2:
        print("Documents are identical (canonical form).")
        return True
    else:
        print("Documents differ (canonical form).")
        return False


compare_xml_canonical('config1.xml', 'config2.xml')

Output:

Documents differ (canonical form).

Canonicalizing from Strings

import xml.etree.ElementTree as ET
import io

def canonicalize_string(xml_string):
    """Canonicalize XML from string input."""
    output = io.StringIO()
    ET.canonicalize(xml_data=xml_string, out=output)
    return output.getvalue()

xml1 = '<root attr2="b" attr1="a"><child/></root>'
xml2 = '<root attr1="a" attr2="b"><child></child></root>'

print(canonicalize_string(xml1) == canonicalize_string(xml2))  # True

Element-by-Element Comparison

For custom comparison logic or detailed reporting:

import xml.etree.ElementTree as ET

def elements_equal(e1, e2):
    """Recursively compare two XML elements."""
    if e1.tag != e2.tag:
        return False, f"Tag mismatch: {e1.tag} vs {e2.tag}"
    
    if dict(e1.attrib) != dict(e2.attrib):
        return False, f"Attribute mismatch in <{e1.tag}>"
    
    text1 = (e1.text or '').strip()
    text2 = (e2.text or '').strip()
    if text1 != text2:
        return False, f"Text mismatch in <{e1.tag}>: '{text1}' vs '{text2}'"
    
    children1 = list(e1)
    children2 = list(e2)
    
    if len(children1) != len(children2):
        return False, f"Child count mismatch in <{e1.tag}>"
    
    for c1, c2 in zip(children1, children2):
        equal, msg = elements_equal(c1, c2)
        if not equal:
            return False, msg
    
    return True, "Elements are equal"

def compare_xml_elements(file1, file2):
    """Compare two XML files element by element."""
    tree1 = ET.parse(file1)
    tree2 = ET.parse(file2)
    
    return elements_equal(tree1.getroot(), tree2.getroot())

is_equal, message = compare_xml_elements('config1.xml', 'config2.xml')
print(message)

Ignoring Specific Elements or Attributes

Sometimes certain fields (like timestamps) should be excluded:

import xml.etree.ElementTree as ET
import copy

def remove_elements(root, xpath_list):
    """Remove specified elements before comparison."""
    root = copy.deepcopy(root)
    
    for xpath in xpath_list:
        for elem in root.findall(xpath):
            parent = root.find(f".//{elem.tag}/..")
            if parent is not None:
                parent.remove(elem)
    
    return root

def compare_xml_filtered(file1, file2, ignore_tags=None):
    """Compare XML files, ignoring specified elements."""
    ignore_tags = ignore_tags or []
    
    tree1 = ET.parse(file1)
    tree2 = ET.parse(file2)
    
    root1 = tree1.getroot()
    root2 = tree2.getroot()
    
    for tag in ignore_tags:
        for elem in root1.findall(f".//{tag}"):
            elem.getparent().remove(elem)
        for elem in root2.findall(f".//{tag}"):
            elem.getparent().remove(elem)
    
    output1 = io.BytesIO()
    output2 = io.BytesIO()
    
    ET.canonicalize(xml_data=ET.tostring(root1), out=output1)
    ET.canonicalize(xml_data=ET.tostring(root2), out=output2)
    
    return output1.getvalue() == output2.getvalue()

Comparing with lxml (Advanced Features)

For complex XML with namespaces or schemas:

pip install lxml
from lxml import etree

def compare_with_lxml(file1, file2):
    """Compare XML using lxml's robust parsing."""
    parser = etree.XMLParser(remove_blank_text=True)
    
    tree1 = etree.parse(file1, parser)
    tree2 = etree.parse(file2, parser)
    
    canon1 = etree.tostring(tree1, method='c14n')
    canon2 = etree.tostring(tree2, method='c14n')
    
    return canon1 == canon2

def get_xpath_differences(file1, file2):
    """Find differences and report XPath locations."""
    tree1 = etree.parse(file1)
    tree2 = etree.parse(file2)
    
    differences = []
    
    def compare_nodes(node1, node2, path=""):
        current_path = f"{path}/{node1.tag}"
        
        if node1.text != node2.text:
            differences.append({
                'xpath': current_path,
                'type': 'text',
                'old': node1.text,
                'new': node2.text
            })
        
    
    compare_nodes(tree1.getroot(), tree2.getroot())
    return differences

Summary

Method Package Ignores Whitespace Ignores Attr Order Best For
xmldiff Third-party Yes Yes Detailed change reports
Canonicalization Standard lib Yes Yes Simple equality checks
Element recursion Standard lib Configurable Yes Custom comparison logic
lxml Third-party Yes Yes Complex XML, namespaces
Navigation