Python Basics: Query and Process XML with ElementTree
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 1Save 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")) == 2Run it:
python3 -B query_namespaced_catalog.pyOutput:
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.pyOutput:
Remaining: 3
<tasks><task id="3" state="open" owner="Build & 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) == 0Run it:
python3 -B process_xml_records_incrementally.pyOutput:
A: 3
B: 7
Total: 10
Retained records: 0An 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_dirIf rmdir reports remaining files, inspect them instead of deleting unknown contents.
References
Acknowledgements
This post was created with assistance from AI.