XML Recipes
Contents
- Create a document with namespaces
- Read and extract (XPath)
- Modify: insert, remove, rename, move
- Validate against an XSD
- Streaming huge files
- Convert XML ↔ dict/JSON
- Gotchas
Create a document with namespaces
import xml.etree.ElementTree as ET
NS = "http://example.com/catalog"
ET.register_namespace("", NS) # default namespace, no prefix in output
root = ET.Element(f"{{{NS}}}catalog")
book = ET.SubElement(root, f"{{{NS}}}book", {"id": "bk101"})
ET.SubElement(book, f"{{{NS}}}title").text = "XML Developer's Guide"
ET.SubElement(book, f"{{{NS}}}price").text = "44.95"
ET.indent(root) # 2-space pretty print (3.9+)
ET.ElementTree(root).write("catalog.xml",
xml_declaration=True, encoding="utf-8")Read and extract (XPath)
ElementTree supports a subset of XPath — child paths, //, [@attr], [tag='text'], positional [1]:
tree = ET.parse("catalog.xml")
root = tree.getroot()
ns = {"c": "http://example.com/catalog"}
first = root.find("c:book[1]", ns) # first book
cheap = root.findall(".//c:book[c:price='44.95']", ns)
ids = [b.get("id") for b in root.findall(".//c:book", ns)]
text = root.findtext(".//c:book/c:title", default="", namespaces=ns)Full XPath 1.0 (functions, contains(), axes) needs lxml:
from lxml import etree
root = etree.parse("catalog.xml").getroot()
titles = root.xpath("//c:book[contains(c:title,'Guide')]/c:title/text()",
namespaces={"c": "http://example.com/catalog"})Modify: insert, remove, rename, move
tree = ET.parse("catalog.xml")
root = tree.getroot()
ns = {"c": "http://example.com/catalog"}
# Insert after an existing child (ElementTree has no insert-after: use index)
books = root.findall("c:book", ns)
new = ET.Element(f"{{http://example.com/catalog}}book", {"id": "bk102"})
root.insert(list(root).index(books[-1]) + 1, new)
# Remove — you must remove from the PARENT
for bad in root.findall("c:book[@id='bk101']", ns):
root.remove(bad)
# Rename a tag
for el in root.iter(f"{{http://example.com/catalog}}price"):
el.tag = f"{{http://example.com/catalog}}cost"
tree.write("catalog.xml", xml_declaration=True, encoding="utf-8")To find a parent when you only matched the child (stdlib has no getparent()):
parents = {c: p for p in root.iter() for c in p}
parents[child].remove(child)Validate against an XSD
Requires lxml (pip install lxml):
from lxml import etree
schema = etree.XMLSchema(etree.parse("catalog.xsd"))
doc = etree.parse("catalog.xml")
if not schema.validate(doc):
for err in schema.error_log:
print(f"line {err.line}, col {err.column}: {err.message}")Report every error with line/column; fix the tree, re-serialize, re-validate.
Streaming huge files
Load-then-clear keeps memory flat regardless of file size:
import xml.etree.ElementTree as ET
for event, elem in ET.iterparse("huge.xml", events=("end",)):
if elem.tag.endswith("record"):
process(elem)
elem.clear() # release children; keeps memory boundedConvert XML ↔ dict/JSON
No stdlib one-liner exists; for flat, repetitive data write it explicitly:
rows = [{"id": b.get("id"), "title": b.findtext("c:title", "", ns)}
for b in root.findall(".//c:book", ns)]
import json; json.dump(rows, open("books.json", "w"), indent=2)For arbitrary nesting, pip install xmltodict and xmltodict.parse(open("f.xml", "rb")) — but beware: single children become dicts, repeated children become lists, so downstream code must handle both shapes.
Gotchas
ns0:prefix pollution. ElementTree inventsns0:prefixes on write unless you callET.register_namespace(prefix, uri)before parsing/writing. Register""for a default namespace.- Truthiness trap. An element with no children is falsy:
if elem:is False even when the element exists. Always useif elem is not None:. find()vsfindall()with no namespace map silently return nothing on namespaced documents — the tag you search must be{uri}tagor use thensmap. "It finds nothing" almost always means a missing namespace map.- No parent pointers in stdlib ElementTree — removal requires the parent (see recipe above); lxml elements have
.getparent(). - Comments and processing instructions are dropped by the default ElementTree parser. If they must survive a round-trip, use lxml, which preserves them.
ET.tostring()omits the XML declaration unlessxml_declaration=True— and returns bytes when an encoding is given.- Entity attacks (billion laughs, external entities). Never parse untrusted XML with plain ElementTree/lxml defaults; use defusedxml drop-ins (
defusedxml.ElementTree.parse). ET.indent()mutates text nodes — don't use it on documents where whitespace inside elements is significant (mixed content).