xml.etree.ElementTree provides a simple and efficient API for reading, querying, modifying, and generating XML. It represents a document as a tree: ElementTree wraps the document and each Element represents a tag with attributes, text, tail text, and children.
ElementTree is useful for configuration files, feeds, legacy integrations, project formats, and other XML-based documents. The official documentation warns that unauthenticated XML can be dangerous. Enforce limits for size, depth, processing time, and external resources.
Reading an XML file
import xml.etree.ElementTree as ET
tree = ET.parse("catalog.xml")
root = tree.getroot()
print(root.tag)
print(root.attrib)
parse() returns an ElementTree. For a small string already in memory, use fromstring():
xml = "<product id='7'><name>Keyboard</name></product>"
root = ET.fromstring(xml)
Do not read an arbitrarily large file before applying a byte limit. Validate size at the storage, upload, or HTTP layer before passing data to the parser.
Tags, attributes, text, and tail
An element exposes tag, attrib, text, and tail. text contains content before the first child, while tail contains content after the closing tag and before the next sibling.
for child in root:
print(child.tag, child.get("id"), child.text, child.tail)
To collect all inner text from a subtree, use "".join(element.itertext()). Do not assume that all visible text lives only in .text.
Finding elements
for product in root.findall("product"):
name = product.findtext("name", default="")
price = product.findtext("price", default="0")
print(name, price)
find() returns the first matching element or None. Test with element is None. Truth-value testing of empty elements is deprecated and can confuse an empty element with a missing one.
Recursive iteration
for link in root.iter("link"):
print(link.get("href"))
iter() walks the entire subtree in document order. For untrusted or very large input, count processed elements and stop when the contract limit is exceeded.
Limited XPath support
ElementTree supports a useful subset of XPath for paths, descendants, attributes, text predicates, and positions.
active = root.findall(".//product[@active='yes']")
second_items = root.findall(".//item[2]")
It is not a full XPath engine. Avoid constructing expressions from arbitrary user text. Prefer known queries and validate values before interpolation.
Namespaces
During parsing, namespaced tags are expanded into {URI}localname.
namespaces = {
"atom": "http://www.w3.org/2005/Atom",
}
for entry in root.findall("atom:entry", namespaces):
title = entry.findtext("atom:title", namespaces=namespaces)
print(title)
Define your own prefix mapping. The original prefix is not the identity of a namespace; the namespace URI is.
Building XML
root = ET.Element("catalog", {"version": "1"})
product = ET.SubElement(root, "product", {"id": "7"})
ET.SubElement(product, "name").text = "Keyboard"
ET.SubElement(product, "price").text = "199.90"
ET.indent(root, space=" ")
xml = ET.tostring(root, encoding="unicode")
print(xml)
The serializer escapes text and attribute values. Still validate dynamically chosen tag names and structure. Do not let untrusted users choose arbitrary element names without an allowlist.
Writing to a file
tree = ET.ElementTree(root)
tree.write(
"catalog.xml",
encoding="utf-8",
xml_declaration=True,
)
Write to a temporary file in the same directory and replace the final destination after success. Read Python tempfile for atomic patterns.
Modifying elements
for product in root.findall("product"):
product.set("reviewed", "yes")
price = product.find("price")
if price is not None:
price.text = str(round(float(price.text or "0"), 2))
When removing children, collect them first and then modify the tree. Changing a collection during iteration can skip elements.
remove = [
product
for product in root.findall("product")
if product.get("inactive") == "yes"
]
for product in remove:
root.remove(product)
Incremental parsing with iterparse
iterparse() can process large documents without keeping all useful nodes in memory, although it performs blocking reads.
for event, element in ET.iterparse(
"data.xml",
events=("end",),
):
if element.tag == "record":
process(element)
element.clear()
Use end events when you need complete children and text. At a start event, contents may not be available. Calling clear() after processing releases references and reduces memory use.
XMLPullParser for chunked input
parser = ET.XMLPullParser(events=("end",))
for block in source_blocks():
parser.feed(block)
for event, element in parser.read_events():
if element.tag == "record":
process(element)
element.clear()
parser.close()
XMLPullParser fits applications where another layer controls non-blocking reads. You still need limits for total bytes, element count, text length, and depth.
Resource limits
Before parsing, cap bytes. During parsing, count elements, attributes, depth, namespace declarations, and text size. Abort when any contract threshold is exceeded.
MAX_ELEMENTS = 100_000
count = 0
for event, element in ET.iterparse("data.xml", events=("end",)):
count += 1
if count > MAX_ELEMENTS:
raise ValueError("XML exceeds element limit")
Untrusted XML
XML can exploit entity expansion, external references, excessive depth, and huge payloads depending on the parser and environment. For third-party input, consider a hardened XML library, process isolation, and infrastructure-level limits.
Successful parsing is not validation. ElementTree does not automatically validate an XSD or guarantee that the document follows your business schema.
XInclude
xml.etree.ElementInclude can replace XInclude nodes with files or resources. The default loader reads from disk. Do not process XInclude from untrusted XML without a restrictive loader, allowed root, and depth cap.
from xml.etree import ElementInclude
ElementInclude.include(
root,
loader=controlled_loader,
max_depth=3,
)
Do not pass max_depth=None for untrusted data. Reject absolute paths, URLs, and traversal attempts.
Canonicalization
canonicalize() applies C14N 2.0 and produces a deterministic representation useful for comparisons and signature workflows.
normalized = ET.canonicalize(
xml_data=original_xml,
with_comments=False,
strip_text=False,
)
Canonicalization does not verify a signature or make a document trustworthy. It only normalizes serialization choices such as namespace placement, attribute ordering, and whitespace.
Attribute order
The XML information model does not assign meaning to attribute order. Current ElementTree versions preserve insertion order, but business logic should never depend on it. Use canonicalization for cryptographic or byte-for-byte output.
Encoding
tostring(..., encoding="unicode") returns str; other encodings return bytes. Match the result type to the file stream. Do not write strings to a binary stream or bytes to a text stream.
For encoding problems, read Python codecs.
Parsing errors
Malformed XML raises ET.ParseError, which commonly includes a line and column.
try:
root = ET.fromstring(received_xml)
except ET.ParseError as error:
line, column = error.position
raise ValueError(
f"invalid XML at line {line}, column {column}"
) from error
Do not expose sensitive XML snippets in public error messages.
Domain validation
After parsing, validate required tags, types, numeric ranges, cardinality, and relationships. Use Decimal for financial values, explicit timezone rules for dates, and allowlists for enums and URLs.
Recommended tests
Test empty documents, malformed XML, namespaces, missing attributes, tail text, supported and unsupported XPath, large files, excessive depth, too many elements, encoding mismatches, atomic writes, blocked XInclude, and canonicalization.
Common mistakes
Frequent mistakes include loading unlimited XML, ignoring namespaces, assuming .text contains all content, testing elements with if not element, modifying children during iteration, expecting full XPath, enabling unrestricted XInclude, relying on attribute order, and treating parsing as validation.
Conclusion
ElementTree is a practical standard-library XML solution. Use parse() and fromstring() for small documents, iterparse() for large files, and XMLPullParser for incremental input. Model namespaces explicitly, validate the domain, and enforce limits before and during parsing.
Read the official ElementTree documentation and Python’s XML security guidance. For external XML, combine an updated parser, limits, validation, and isolation.







