Skip to content
Python Basics: Query and Process XML with ElementTree

Python Basics: Query and Process XML with ElementTree

October 23, 2026

A small XML export can contain namespaces, optional fields, and repeated records. ElementTree lets a script query that structure without treating markup as plain text. The examples below use fixed, trusted XML strings so you can focus on names, mutation, and incremental processing.

Prepare a scratch directory

These standalone examples were tested on Linux with Python 3.12.14. Use Python 3.8 or newer. The examples use only the standard library and keep all XML data in memory. Use Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Run without -O so the example assertions remain enabled.

Create a fresh directory and keep using the same terminal:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each named file below as UTF-8. Do not name your script after a standard-library module.

1. Find elements inside a namespace

Save as query_namespaced_catalog.py:

import xml.etree.ElementTree as ET

document = '<catalog xmlns="urn:demo:catalog"><item id="A"><name>Notebook</name></item><item id="B"/></catalog>'
root = ET.fromstring(document)
namespaces = {"c": "urn:demo:catalog"}
print("Unqualified matches:", len(root.findall("item")))
for item in root.findall("c:item", namespaces):
    name = item.find("c:name", namespaces)
    label = "(missing)" if name is None else (name.text or "")
    print(f"{item.get('id')}: {label}")
assert len(root.findall("{urn:demo:catalog}item")) == 2

Run it:

python3 -B query_namespaced_catalog.py

Output:

Unqualified matches: 0
A: Notebook
B: (missing)

The default namespace becomes part of each element tag. Supply a namespace mapping or an expanded {URI}name when querying. The c prefix is a local query alias, not a requirement on the original document. Test name is None for absence: an existing element can have no children and no text.

2. Remove selected children from a snapshot

Save as remove_finished_xml_items.py:

import xml.etree.ElementTree as ET

root = ET.fromstring('<tasks><task id="1" state="done"/><task id="2" state="done"/><task id="3" state="open"/></tasks>')
for task in list(root.findall("task")):
    if task.get("state") == "done":
        root.remove(task)
remaining = [task.get("id") for task in root.findall("task")]
assert remaining == ["3"]
root[0].set("owner", "Build & Test")
serialized = ET.tostring(root, encoding="unicode")
print("Remaining:", ", ".join(remaining))
print(serialized)
assert ET.fromstring(serialized)[0].get("owner") == "Build & Test"

Run it:

python3 -B remove_finished_xml_items.py

Output:

Remaining: 3
<tasks><task id="3" state="open" owner="Build &amp; Test" /></tasks>

Iterate over a separate list while removing children, rather than changing the live child sequence beneath a direct iteration. The serializer escapes the ampersand in the attribute. Compare parsed values when semantics matter; ordinary serialization is not a byte-for-byte round trip or XML canonicalization.

3. Release completed direct-child records

Save as process_xml_records_incrementally.py:

from io import BytesIO
import xml.etree.ElementTree as ET

document = b"<records><record id='A'><value>3</value></record><record id='B'><value>7</value></record></records>"
events = ET.iterparse(BytesIO(document), events=("start", "end"))
_, root = next(events)
total = 0
for event, element in events:
    if event == "end" and element.tag == "record":
        value = int(element.findtext("value", default="0"))
        total += value
        print(f"{element.get('id')}: {value}")
        root.remove(element)
        element.clear()
print("Total:", total)
print("Retained records:", len(root))
assert total == 10 and len(root) == 0

Run it:

python3 -B process_xml_records_incrementally.py

Output:

A: 3
B: 7
Total: 10
Retained records: 0

An end event means the element contents are available. This known document shape places every record directly under the root, so removing it detaches the completed subtree. clear alone would leave an empty record attached to the parent. For a file export, pass a file object instead of the in-memory fixture; iterparse still performs blocking reads.

This streaming pattern reduces retained record data, but does not promise constant memory for arbitrary XML: one record may be huge, the parser may read ahead, and this example already holds its input bytes. Namespaces and nesting must match the real schema. ElementTree is not hardened for malicious XML; use an appropriate hardened parser and input/resource limits for untrusted documents.

Clean up

In the same terminal, remove only the named sources, return to the original directory, and remove the empty scratch directory:

rm -- query_namespaced_catalog.py remove_finished_xml_items.py process_xml_records_incrementally.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports remaining files, inspect them instead of deleting unknown contents.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on