Long before JSON took over as the default data interchange format, XML was everywhere, and it still shows up constantly in enterprise systems, SOAP APIs, RSS feeds, and configuration formats I’ve had to work with over the years. My first attempts at generating XML involved string concatenation, and it went about as well as you’d expect — malformed tags, unescaped special characters, and hours of debugging over a missing closing bracket. Python’s standard library gives me proper tools for this, and this guide covers everything from building simple XML documents to parsing complex ones safely.
Why Not Just Build XML with String Formatting
I want to address this up front because it’s the trap I fell into first.
# DON'T do this - breaks the moment content has special characters
name = "Tom & Jerry's <Adventures>"
bad_xml = f"<title>{name}</title>"
print(bad_xml)
Output:
<title>Tom & Jerry's <Adventures></title>
This is invalid XML — the unescaped &, <, and > characters inside the content break the document structure entirely. A proper XML library automatically escapes these characters, which is one of many reasons I always reach for xml.etree.ElementTree instead of manual string building.
ElementTree: Python’s Built-In XML Toolkit
xml.etree.ElementTree (usually imported as ET) is part of the standard library and is my default tool for both building and parsing XML in Python.
import xml.etree.ElementTree as ET
Building a Simple XML Document
import xml.etree.ElementTree as ET
root = ET.Element("bookstore")
book = ET.SubElement(root, "book")
book.set("category", "fiction")
title = ET.SubElement(book, "title")
title.text = "The Great Gatsby"
author = ET.SubElement(book, "author")
author.text = "F. Scott Fitzgerald"
price = ET.SubElement(book, "price")
price.text = "10.99"
tree = ET.ElementTree(root)
ET.indent(tree, space=" ") # pretty-print with indentation (Python 3.9+)
tree.write("bookstore.xml", encoding="utf-8", xml_declaration=True)
Resulting bookstore.xml:
<?xml version='1.0' encoding='utf-8'?>
<bookstore>
<book category="fiction">
<title>The Great Gatsby</title>
<author>F. Scott Fitzgerald</author>
<price>10.99</price>
</book>
</bookstore>
ET.Element() creates the root node, ET.SubElement() creates and attaches a child node to a parent in one call, .set() adds an XML attribute, and .text sets the text content between the opening and closing tags. This structure — a tree of Element objects — mirrors XML’s own nested structure almost exactly, which made it intuitive for me once I stopped thinking in terms of raw text and started thinking in terms of the tree itself.
Automatic Escaping of Special Characters
This is the detail that sold me on ElementTree permanently over manual string building.
import xml.etree.ElementTree as ET
root = ET.Element("message")
content = ET.SubElement(root, "content")
content.text = "Tom & Jerry's <Adventures> \"begin\""
xml_string = ET.tostring(root, encoding='unicode')
print(xml_string)
Output:
<message><content>Tom & Jerry's <Adventures> "begin"</content></message>
ElementTree automatically escapes & and </> (the characters that would otherwise break XML’s structure) whenever it serializes text content — I never have to remember to call an escaping function myself; it’s handled at the point of serialization.
Building Nested, Multi-Item Documents
import xml.etree.ElementTree as ET
root = ET.Element("bookstore")
books_data = [
{"title": "1984", "author": "George Orwell", "price": "8.99", "category": "dystopian"},
{"title": "Brave New World", "author": "Aldous Huxley", "price": "9.50", "category": "dystopian"},
]
for book_data in books_data:
book = ET.SubElement(root, "book")
book.set("category", book_data["category"])
ET.SubElement(book, "title").text = book_data["title"]
ET.SubElement(book, "author").text = book_data["author"]
ET.SubElement(book, "price").text = book_data["price"]
tree = ET.ElementTree(root)
ET.indent(tree, space=" ")
tree.write("bookstore_full.xml", encoding="utf-8", xml_declaration=True)
Looping through a list of dictionaries and building SubElement nodes programmatically is the pattern I use almost every time I’m generating XML from real application data, rather than hand-writing it.
Getting XML as a String Instead of Writing to a File
import xml.etree.ElementTree as ET
root = ET.Element("status")
ET.SubElement(root, "code").text = "200"
ET.SubElement(root, "message").text = "OK"
xml_string = ET.tostring(root, encoding='unicode')
print(xml_string)
I use this when generating XML for an API response body, rather than writing to disk at all — tostring() gives me the raw string to send back directly.
Parsing XML: Reading a File
import xml.etree.ElementTree as ET
tree = ET.parse('bookstore.xml')
root = tree.getroot()
print(f"Root tag: {root.tag}")
for book in root.findall('book'):
category = book.get('category')
title = book.find('title').text
author = book.find('author').text
price = book.find('price').text
print(f"[{category}] {title} by {author} - ${price}")
Output:
Root tag: bookstore
[fiction] The Great Gatsby by F. Scott Fitzgerald - $10.99
find() returns the first matching child element (or None if not found), and findall() returns all matching children as a list. .get() retrieves an XML attribute by name, while .text retrieves the element’s text content.
Parsing XML from a String
import xml.etree.ElementTree as ET
xml_data = '<person><name>Alice</name><age>30</age></person>'
root = ET.fromstring(xml_data)
print(root.find('name').text)
print(root.find('age').text)
ET.fromstring() parses an XML string directly into an Element tree without touching the filesystem at all — useful for handling XML received over a network, like an API response.
Navigating Deeply Nested XML with XPath-Like Syntax
ElementTree supports a limited but genuinely useful subset of XPath syntax for locating elements without manually walking the tree level by level.
import xml.etree.ElementTree as ET
xml_data = '''
<library>
<section name="fiction">
<book><title>Dune</title><price>12.99</price></book>
<book><title>Foundation</title><price>11.50</price></book>
</section>
<section name="nonfiction">
<book><title>Sapiens</title><price>15.00</price></book>
</section>
</library>
'''
root = ET.fromstring(xml_data)
# Find all book titles anywhere in the document, regardless of nesting depth
for title in root.findall('.//title'):
print(title.text)
# Find books only within a specific section using an attribute filter
fiction_section = root.find(".//section[@name='fiction']")
for book in fiction_section.findall('book'):
print(f"Fiction: {book.find('title').text}")
Output:
Dune
Foundation
Sapiens
Fiction: Dune
Fiction: Foundation
The .// prefix means “search recursively at any depth,” and [@name='fiction'] is an attribute-based filter — both borrowed from real XPath syntax, though ElementTree only implements a subset of the full XPath specification.
Modifying Existing XML
import xml.etree.ElementTree as ET
tree = ET.parse('bookstore.xml')
root = tree.getroot()
for book in root.findall('book'):
price = book.find('price')
price.text = str(float(price.text) * 1.1) # apply a 10% price increase
tree.write('bookstore_updated.xml', encoding='utf-8', xml_declaration=True)
Since ElementTree builds an in-memory tree of mutable Element objects, modifying attributes, text, or even adding/removing child elements is just regular Python object manipulation — no special “edit mode” is needed, I just change the objects and write the tree back out.
Handling XML Namespaces
Namespaces are the part of XML that consistently trips people up, myself included for a long time, because tag names change once a namespace is involved.
import xml.etree.ElementTree as ET
xml_data = '''<?xml version="1.0"?>
<root xmlns:book="http://example.com/book">
<book:title>Python Programming</book:title>
</root>'''
root = ET.fromstring(xml_data)
namespaces = {'book': 'http://example.com/book'}
title = root.find('book:title', namespaces)
print(title.text)
Without registering the namespace mapping and passing it to find(), searching for 'title' alone silently fails to match anything, since internally the element’s actual tag becomes {http://example.com/book}title — the namespace URI wrapped in curly braces gets prepended to the local tag name once parsed.
Security: Why Parsing Untrusted XML Needs Extra Care
This is a detail I didn’t appreciate until I read more deeply into it, and it matters enough that I want to flag it explicitly. Standard XML parsers, including the default configuration of some XML libraries, can be vulnerable to XML External Entity (XXE) attacks, where a maliciously crafted XML document references external entities that cause the parser to read arbitrary local files or make unwanted network requests.
Python’s xml.etree.ElementTree, since Python 3.7.1 and later security patches, disables external entity resolution by default, which mitigates the classic XXE attack. Still, when I’m parsing XML from a genuinely untrusted source (user uploads, external APIs I don’t fully control), I stay cautious and, for especially sensitive applications, consider the defusedxml third-party package, which is specifically hardened against a broader range of known XML-based attacks beyond what the standard library defends against by default.
# For untrusted XML input, consider defusedxml instead of the standard library directly
# pip install defusedxml
from defusedxml import ElementTree as SafeET
tree = SafeET.parse('untrusted_input.xml')
Common Mistakes I’ve Made
- Building XML with string concatenation, breaking on any content with
&,<, or>characters. - Forgetting namespace prefixes when searching parsed XML, and being confused why
find()returnedNoneon an element I could clearly see in the file. - Not handling
Nonereturns fromfind(), then getting anAttributeErrortrying to access.texton nothing. - Ignoring XXE risks entirely when parsing XML from an untrusted source, especially with older Python versions or third-party XML libraries with less safe defaults.
- Confusing
.textand.tail—.tailholds any text that comes after a closing tag but before the next sibling’s opening tag, which surprises people the first time whitespace-sensitive parsing produces unexpected results.
result = root.find('nonexistent_tag')
if result is not None:
print(result.text)
else:
print("Element not found")
Real-World Use Cases
- Generating RSS/Atom feeds for a website or blog.
- Producing SOAP API request/response bodies for legacy enterprise system integration.
- Reading and transforming configuration files written in XML.
- Parsing data exports from systems (like certain government, financial, or scientific data formats) that still standardize on XML.
FAQs
Should I use ElementTree or lxml? ElementTree is built into the standard library and sufficient for most tasks. lxml (a third-party package) offers full XPath 1.0 support, better performance on very large documents, and XML Schema validation, at the cost of an external dependency.
Why did my search for an element return None unexpectedly? Most commonly a namespace issue — the actual internal tag name includes the namespace URI, and searches need the namespace mapping passed explicitly to find()/findall().
Is it safe to parse XML from user uploads? The standard library’s default entity-handling behavior is reasonably safe in modern Python versions, but for genuinely untrusted input, defusedxml provides stronger protection against a wider range of known XML attack vectors.
How do I pretty-print XML with indentation? Use ET.indent(tree, space=" ") (available since Python 3.9) before writing, or use xml.dom.minidom for pretty-printing on older Python versions.
Can ElementTree validate XML against a schema (XSD/DTD)? No, not natively — ElementTree focuses on parsing and building, not schema validation. For validation, lxml supports XSD/DTD validation directly.
Summary
Building and parsing XML in Python is far more reliable through xml.etree.ElementTree than through manual string manipulation, since it handles character escaping, tree structure, and serialization correctly by construction rather than by careful discipline on my part. Understanding how namespaces change tag names internally, and being deliberate about security when parsing untrusted XML, rounds out what I consider the essential knowledge for working with XML confidently in Python — a format that, despite JSON’s dominance, still shows up often enough in real systems that it’s worth knowing properly.
References
- Python Official Documentation: xml.etree.ElementTree — The ElementTree XML API
- Python Official Documentation: XML vulnerabilities
- defusedxml Documentation: https://pypi.org/project/defusedxml/