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.
Recommended practices
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.







