ElementTree: Read and Modify XML in Python

Published on: August 22, 2026
Reading time: 5 minutes
A solitary tree with sunlight filtering through the leaves on a grassy hill under a blue sky.

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.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python poplib: Read Email with POP3

    Learn Python poplib to access POP3 with TLS, list and download messages, use UIDL, enforce limits, and avoid accidental deletion.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python imaplib: Read Email with IMAP

    Learn Python imaplib to access IMAP mailboxes with TLS, search by UID, read without marking messages, manage flags, and avoid

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Extreme close-up of computer code displaying various programming terms and elements.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ftplib: Secure FTP and FTPS

    Learn Python ftplib to list, download, and upload files over FTP or FTPS with TLS, timeouts, limits, resume, and clear

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Server rack representing an endpoint built with Python xmlrpc.server
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    xmlrpc.server: Build XML-RPC Servers

    Learn Python xmlrpc.server to build XML-RPC servers, register functions, restrict methods and paths, and avoid unsafe exposure.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Server cables representing remote calls with Python xmlrpc.client
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xmlrpc.client: Remote Calls

    Learn Python xmlrpc.client to call XML-RPC services, handle Fault and ProtocolError, use TLS, compatible types, and safe limits.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026
    Error code over binary data representing failures handled with Python urllib.error
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python urllib.error: Handle HTTP Failures

    Learn Python urllib.error to handle URLError, HTTPError, incomplete downloads, selective retries, and clearer network diagnostics.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026