ElementTree canonicalize: Normalize XML in Python

Published on: October 22, 2026
Reading time: 5 minutes
Code and XML structure for canonicalization with Python

Signing, comparing, or versioning XML documents requires more than saving the same tree twice. Whitespace, attribute order, namespace declarations, and other details can change the textual representation without changing the data’s meaning. xml.etree.ElementTree.canonicalize() solves this problem by producing a canonical XML form suitable for stable comparisons, auditing, and preparing content for digital signatures.

This guide explains XML canonicalization, the main ElementTree.canonicalize options, practical hashing and testing patterns, and the security and compatibility limits that matter in production.

What canonical XML means

Two XML documents can represent the same structure while having different text. Attributes may appear in another order, namespace prefixes may differ, and insignificant whitespace may vary. A direct string comparison treats them as different. Canonicalization transforms XML into a predictable representation according to C14N rules.

This is useful for tests, caches, deduplication, source control, and systems that calculate digests. It is also a common step before signing XML because any later textual difference changes the cryptographic hash.

Basic example

from xml.etree.ElementTree import canonicalize

xml = '''
<order xmlns:x="https://example.com/item" id="42" status="paid">
  <x:product quantity="2" code="A1" />
</order>
'''

result = canonicalize(xml_data=xml)
print(result)

The function accepts XML through xml_data and returns Unicode text. You can also provide an output file through out. The result normalizes details that should not affect structural equivalence.

Reading from a file

from xml.etree.ElementTree import canonicalize

with open("input.xml", encoding="utf-8") as source:
    with open("canonical.xml", "w", encoding="utf-8") as target:
        canonicalize(from_file=source, out=target)

When the XML is already stored on disk, from_file avoids manual loading. Open the destination in text mode with UTF-8 so the output remains consistent across environments.

Keeping or removing comments

Comments often do not participate in the business meaning of a document. The with_comments option controls whether they remain in the result.

canonical = canonicalize(
    xml_data=xml,
    with_comments=False,
)

Removing comments is common for hashes and configuration comparisons. In audit or document workflows, comments may be meaningful and should be retained.

Whitespace and strip_text

The strip_text option removes leading and trailing whitespace from text content. Use it only when that whitespace is not meaningful to the domain.

canonical = canonicalize(
    xml_data="<name>  Ana  </name>",
    strip_text=True,
)

This may be convenient for configuration XML. In editorial documents, messages, or formats where spacing matters, stripping can change the actual content. Test the policy with real samples.

Rewriting namespace prefixes

Different prefixes can point to the same namespace. The rewrite_prefixes option replaces original prefixes with predictable names such as n0 and n1.

canonical = canonicalize(
    xml_data=xml,
    rewrite_prefixes=True,
)

This helps when equivalent documents come from systems that choose different prefixes. The namespace URI remains the real identifier; the prefix is only an abbreviation.

QName-aware tags and attributes

Some XML formats store qualified names inside attribute values or element text. In those cases, the canonicalizer must know which locations contain QNames. Use qname_aware_tags and qname_aware_attrs.

canonical = canonicalize(
    xml_data=xml,
    rewrite_prefixes=True,
    qname_aware_attrs={"type"},
    qname_aware_tags={"expression"},
)

Without this configuration, a namespace prefix may be rewritten in the XML structure but remain unchanged inside text, producing a semantic mismatch.

Excluding volatile fields

The exclude_tags and exclude_attrs options can ignore volatile values such as timestamps, tracing identifiers, or generated metadata.

canonical = canonicalize(
    xml_data=xml,
    exclude_attrs={"generated_at", "request_id"},
    exclude_tags={"debug"},
)

This is useful in snapshot tests and deduplication, but it must be controlled carefully. Excluding meaningful fields can make different documents appear identical. Document the policy and review it as part of the protocol.

Building stable hashes

from hashlib import sha256
from xml.etree.ElementTree import canonicalize

def xml_hash(xml_text: str) -> str:
    canonical = canonicalize(
        xml_data=xml_text,
        with_comments=False,
        rewrite_prefixes=True,
    )
    return sha256(canonical.encode("utf-8")).hexdigest()

The hash is stable only when every participant uses exactly the same canonicalization options. Store a policy version next to the digest or include it in the protocol documentation.

Comparing XML in tests

def xml_equivalent(a: str, b: str) -> bool:
    options = {
        "with_comments": False,
        "rewrite_prefixes": True,
        "strip_text": True,
    }
    return canonicalize(xml_data=a, **options) == canonicalize(xml_data=b, **options)

This is more reliable than comparing raw XML strings, but it does not replace schema validation or business rules. Canonically equal documents are textually equivalent under the selected policy; they are not automatically valid for a particular API.

Canonicalization and digital signatures

Canonicalization is only one part of XML signatures. It does not calculate the signature, validate certificates, or protect against every transformation. Signature systems must follow the exact algorithm, transforms, and profile required by the protocol. For critical scenarios, use a library designed for XML Signature and test interoperability with other implementations.

Do not invent a cryptographic protocol by simply combining canonicalize() with a private key. Use established standards and expert review.

Parsing security

Untrusted XML can cause excessive resource use or exploit unsafe parser configurations. Limit document size, validate sources, apply timeouts, and consider hardened XML libraries for hostile input. Canonicalization does not sanitize XML and does not make unsafe input trustworthy.

For broader context, read Python ElementTree, secure hashes with hashlib, file handling with pathlib, and the Advanced Python category.

Create one central canonicalization function instead of scattering option combinations throughout the codebase. Test namespaces, reordered attributes, comments, whitespace, Unicode, and excluded fields. Record the policy and never change it silently when historical hashes depend on the old representation.

Persist output as UTF-8 text. For large XML documents, enforce memory limits and measure the cost of holding complete strings in memory. Keep canonicalization, schema validation, business validation, and cryptographic signing as separate, explicit steps.

External references

See the official ElementTree.canonicalize documentation and the W3C XML Canonicalization recommendation.

Conclusion

ElementTree.canonicalize turns XML into a stable textual form. It is valuable for testing, hashing, auditing, and integration with systems that require canonical representation. The output depends on the selected options, so treat configuration as a formal policy. With validation, security limits, and interoperability tests, canonicalization reduces false differences and makes XML pipelines more predictable.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python programming code displayed on a screen
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    T-strings and templatelib: Structured Interpolation

    Learn Python t-strings and string.templatelib for structured interpolation, safer renderers, logging, and custom processing.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026
    SQLite database and Python code for selective iterdump export
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    SQLite iterdump: Create Selective Dumps in Python

    Learn sqlite3.Connection.iterdump filters for selective SQLite dumps, dependency checks, validation, and safer restoration.

    Ler mais

    Tempo de leitura: 4 minutos
    21/10/2026
    Code and files representing Python data compression
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Fast Zstandard in Python

    Learn Python compression.zstd for fast file and data compression, streaming, integrity checks, and safe decompression.

    Ler mais

    Tempo de leitura: 7 minutos
    21/10/2026
    Configuration file and code structure for Python ConfigParser
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ConfigParser: Read INI Files Without Sections

    Learn Python ConfigParser unnamed sections with validation, typed values, compatibility, layered settings, and safe INI writing.

    Ler mais

    Tempo de leitura: 5 minutos
    20/10/2026
    Binary data and computer memory representing Python memoryview
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview count and index: Search Without Copies

    Learn Python memoryview count and index to search values in zero-copy buffers and validate binary data efficiently.

    Ler mais

    Tempo de leitura: 5 minutos
    20/10/2026
    Developer working with Python code and data validation
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    map strict: Detect Misaligned Iterables

    Learn Python map strict to validate iterables, prevent silent truncation, and detect misaligned data early.

    Ler mais

    Tempo de leitura: 4 minutos
    19/10/2026