To work with XML in Python, reach for the standard library xml.etree.ElementTree for almost everything: use ET.parse() or ET.fromstring() to read a document, and Element / SubElement plus ElementTree.write() to build one. Pick lxml when you need full XPath, XSLT, or maximum speed; xmltodict when you would rather treat XML as nested Python dicts; and iterparse or SAX when a file is too large to hold in memory.
One rule overrides all of the above: if the XML comes from an untrusted source (a user upload, a third-party API, a webhook), parse it with defusedxml. Python's stdlib parsers are not safe against malicious XML by default.
This guide covers parsing, building, streaming, the security pitfalls, and how to choose between the libraries, with runnable Python 3.13 examples.
Parsing XML with ElementTree
xml.etree.ElementTree is the batteries-included default. You can read from a file or straight from a string.
import xml.etree.ElementTree as ET
# Parse from a file -> ElementTree, then get the root Element
tree = ET.parse("users.xml")
root = tree.getroot()
# Or parse directly from a string -> returns the root Element
xml_text = """<Users version="1.0" language="en">
<user id="1">
<FirstName>Ada</FirstName>
<Email>ada@example.com</Email>
</user>
</Users>"""
root = ET.fromstring(xml_text)
print(root.tag) # 'Users'
print(root.attrib) # {'version': '1.0', 'language': 'en'}Navigating the tree
Iterate an element to get its direct children, use find/findall with tag names or a small XPath subset, and iter() to walk the whole subtree at any depth.
# Direct children: just iterate the element
for user in root:
print(user.tag, user.attrib)
first_user = root.find("user") # first matching child, or None
all_users = root.findall("user") # list of matching children
email = root.find("user/Email").text # path lookup
uid = root.find("user").get("id") # read an attribute
# iter() walks the entire subtree, any depth
for email in root.iter("Email"):
print(email.text)
# findtext() returns text directly, with a default
name = root.findtext("user/FirstName", default="unknown")Note:
Element.getchildren()andgetiterator()were deprecated for years and removed in Python 3.9. Iterate the element directly (for child in elem) or calllist(elem)instead.
ElementTree understands a useful subset of XPath: attribute predicates, text matches, and wildcards.
root.findall(".//user[@id='1']") # users with id == 1
root.findall(".//user/*") # every child of every user
root.findall(".//Email[.='ada@example.com']") # match by element textNamespaces
Pass a prefix-to-URI map to find/findall/iterfind. This is how you read Atom feeds, sitemaps, SOAP, and most enterprise XML.
ns = {"a": "http://www.w3.org/2005/Atom"}
for entry in root.findall("a:entry", ns):
title = entry.findtext("a:title", namespaces=ns)
print(title)Full XPath, XSLT, and speed: lxml
lxml exposes the same ElementTree API but adds full XPath 1.0, XSLT, and DTD/XML-Schema validation, and it is significantly faster on large documents. Install it with pip install lxml.
from lxml import etree
tree = etree.parse("users.xml")
root = tree.getroot()
# Full XPath 1.0: functions, axes, predicates, text() nodes
emails = root.xpath("//user[position() <= 10]/Email/text()")
adas = root.xpath("//user[contains(FirstName, 'Ada')]/FirstName/text()")
# XPath with namespaces
ns = {"a": "http://www.w3.org/2005/Atom"}
titles = root.xpath("//a:entry/a:title/text()", namespaces=ns)Streaming large files: iterparse and SAX
Calling parse() on a multi-gigabyte file loads the entire tree into memory. Instead, stream it. iterparse() yields elements as they finish parsing; call elem.clear() to release each processed record.
import xml.etree.ElementTree as ET
count = 0
for event, elem in ET.iterparse("huge.xml", events=("end",)):
if elem.tag == "user":
count += 1
# ... process elem here ...
elem.clear() # drop this element's children to free memory
print(count)elem.clear() empties the element's own subtree, but the root still references every top-level sibling, so for very long runs also clear the root periodically. For pure event-driven parsing with the lowest footprint, use xml.sax.
import xml.sax
class UserHandler(xml.sax.ContentHandler):
def __init__(self):
self.tag = ""
self.email = ""
def startElement(self, name, attrs):
self.tag = name
def characters(self, content):
if self.tag == "Email":
self.email += content
def endElement(self, name):
if name == "Email":
print(self.email.strip())
self.email = ""
parser = xml.sax.make_parser()
parser.setContentHandler(UserHandler())
parser.parse("huge.xml") # use defusedxml.sax for untrusted inputXML as dictionaries: xmltodict
When the XML is essentially config or a simple record set, xmltodict lets you skip the tree API entirely. Attributes become @-prefixed keys and element text becomes #text. Install with pip install xmltodict.
import xmltodict
with open("users.xml", "rb") as f:
data = xmltodict.parse(f.read())
print(data["Users"]["@version"]) # '1.0'
print(data["Users"]["user"]["Email"]) # 'ada@example.com'
# Round-trip back to XML (use json.dumps(data) for JSON)
xml_string = xmltodict.unparse(data, pretty=True)Building and writing XML with ElementTree
Build a document from Element and SubElement objects, then pretty-print with ET.indent() (added in Python 3.9) and write it with a proper XML declaration and encoding.
import xml.etree.ElementTree as ET
root = ET.Element("Users", attrib={"version": "1.0", "language": "en"})
user = ET.SubElement(root, "user", attrib={"id": "1"})
ET.SubElement(user, "FirstName").text = "Ada"
ET.SubElement(user, "LastName").text = "Lovelace"
ET.SubElement(user, "Email").text = "ada@example.com"
tree = ET.ElementTree(root)
ET.indent(tree, space=" ") # pretty-print, Python 3.9+
tree.write("users.xml", encoding="utf-8", xml_declaration=True)That writes:
<?xml version='1.0' encoding='utf-8'?>
<Users version="1.0" language="en">
<user id="1">
<FirstName>Ada</FirstName>
<LastName>Lovelace</LastName>
<Email>ada@example.com</Email>
</user>
</Users>To get a string instead of writing a file, use tostring(). For namespaced output, declare names in Clark notation ({uri}tag) and register a clean prefix.
# As text (no declaration) or bytes (with declaration)
xml_str = ET.tostring(root, encoding="unicode")
xml_bytes = ET.tostring(root, encoding="utf-8", xml_declaration=True)
# Namespaced output
ATOM = "http://www.w3.org/2005/Atom"
ET.register_namespace("", ATOM) # default ns -> clean output
feed = ET.Element(f"{{{ATOM}}}feed")
entry = ET.SubElement(feed, f"{{{ATOM}}}entry")
ET.SubElement(entry, f"{{{ATOM}}}title").text = "Hello"Which library should you use?
| Tool | Best for | XPath | Streaming | Install |
|---|---|---|---|---|
xml.etree.ElementTree |
Most read/write tasks | Subset | iterparse |
Stdlib |
lxml |
Full XPath/XSLT, validation, speed | Full 1.0 | iterparse |
pip install lxml |
xmltodict |
Treating XML as dicts/JSON | No | No | pip install xmltodict |
xml.sax / iterparse |
Huge files, low memory | No | Yes | Stdlib |
defusedxml |
Any untrusted XML | (wraps above) | Yes | pip install defusedxml |
A practical default: ElementTree for everyday work, lxml when XPath or speed matters, and defusedxml whenever you do not control where the XML came from.
Security: never parse untrusted XML with the stdlib
This is the part most tutorials skip. Python's standard parsers (ElementTree, SAX, minidom) are vulnerable when fed hostile XML:
- Billion laughs / quadratic blowup - a few kilobytes of nested entity definitions expand to gigabytes in memory and crash the process.
- External entity expansion (XXE) - entities that read local files like
file:///etc/passwdor trigger outbound network requests (SSRF).
The fix is defusedxml, a drop-in replacement that disables these dangerous features. Same API, safe defaults.
# pip install defusedxml
from defusedxml.ElementTree import parse, fromstring
tree = parse("untrusted.xml") # raises EntitiesForbidden on attack
root = fromstring(untrusted_bytes) # same API as xml.etree, but safeFor SAX or lxml use defusedxml.sax and defusedxml.lxml. Rule of thumb: every byte of XML from a user upload, partner API, or webhook should pass through defusedxml first.
Real-world examples
Read a sitemap or RSS/Atom feed. Remember the namespace, and use defusedxml for feeds you do not control.
import urllib.request
import xml.etree.ElementTree as ET
url = "https://micropyramid.com/sitemap.xml"
with urllib.request.urlopen(url) as resp:
root = ET.fromstring(resp.read())
ns = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}
urls = [loc.text for loc in root.iterfind(".//sm:loc", ns)]
print(len(urls), "URLs found")Generate an XML payload for a legacy or SOAP API:
import xml.etree.ElementTree as ET
order = ET.Element("Order", attrib={"id": "1001"})
items = ET.SubElement(order, "Items")
for sku, qty in [("A-1", 2), ("B-7", 1)]:
item = ET.SubElement(items, "Item")
ET.SubElement(item, "SKU").text = sku
ET.SubElement(item, "Qty").text = str(qty)
payload = ET.tostring(order, encoding="utf-8", xml_declaration=True)
# requests.post(url, data=payload, headers={"Content-Type": "application/xml"})Where this fits in real projects
XML is still everywhere in enterprise and government integrations: SOAP services, financial messaging, e-invoicing, healthcare records, and legacy ERP exports. At MicroPyramid we have spent 12+ years and 50+ projects building Python applications and integrations - from parsing legacy SOAP and XML feeds to generating compliant payloads for enterprise and public-sector systems. If you are working with structured data in Python, our Python development services and custom software development teams can help, including high-throughput APIs built with FastAPI.
Working with tabular data instead? See our guides on generating CSV and Excel files with Python and creating Excel files with charts using XlsxWriter.
Frequently Asked Questions
Should I use ElementTree or lxml for XML in Python?
Use the standard library xml.etree.ElementTree for most work - it is built in and handles parsing, navigation, and writing well. Choose lxml when you need full XPath 1.0, XSLT transforms, DTD or XML-Schema validation, or higher throughput on large documents. Because lxml mirrors the ElementTree API, switching later is usually a one-line import change.
How do I parse a large XML file without running out of memory?
Do not load the whole tree with parse(). Use xml.etree.ElementTree.iterparse() (or xml.sax) to process elements as a stream, and call elem.clear() after handling each record to release it. For very long runs, also clear the root element periodically so processed siblings are freed.
Why was getchildren() removed and what replaces it?
Element.getchildren() and getiterator() were deprecated for years and removed in Python 3.9. Iterate the element directly (for child in elem) or call list(elem) to get its child elements, and use elem.iter() to walk the entire subtree.
How do I handle XML namespaces in Python?
Pass a prefix-to-URI mapping to find, findall, or iterfind, for example root.findall("a:entry", {"a": "http://www.w3.org/2005/Atom"}). When building documents, use Clark notation {uri}tag for element names and call ET.register_namespace() to control the prefixes in the output.
Is Python's XML parser safe for untrusted input?
No. The stdlib parsers are vulnerable to entity-expansion ("billion laughs") and external-entity (XXE) attacks. For any XML from users, uploads, third-party APIs, or webhooks, parse with defusedxml, a drop-in replacement that disables these dangerous features by default.
How do I convert XML to a dict or JSON in Python?
Use xmltodict.parse() to turn XML into nested dictionaries (attributes become @-prefixed keys and text becomes #text), then json.dumps() if you need JSON. xmltodict.unparse() converts the dict back to XML. It is ideal for config-style XML; for complex documents with mixed content, ElementTree or lxml give you finer control.