xml.etree.ElementTree module
Overview
xml.etree.ElementTree provides a lightweight API for parsing and generating XML. You work with a tree of elements, each representing a single node — with a tag, attributes, and child elements. The module ships with Python 2.7+ and requires no installation.
It is not the fastest XML library available, and it is not safe for untrusted XML (use defusedxml for that). But for reading configuration files, SOAP responses, or structured data from REST APIs, it is usually the right tool.
Parsing XML
From a string
import xml.etree.ElementTree as ET
xml_data = """<config>
<database host="localhost" port="5432"/>
<cache enabled="true" ttl="300"/>
</config>"""
root = ET.fromstring(xml_data)
print(root.tag) # => 'config'
print(root[0].tag) # => 'database'
print(root[0].attrib) # => {'host': 'localhost', 'port': '5432'}
ET.fromstring() parses a string directly into an element tree. It raises ParseError if the XML is malformed.
From a file or URL
tree = ET.parse("config.xml")
root = tree.getroot()
Use ET.parse() for file paths. For HTTP responses or other file-like objects, parse from a stream:
import urllib.request
response = urllib.request.urlopen("https://example.com/data.xml")
root = ET.parse(response).getroot()
Iterating over all elements
root = ET.parse("config.xml").getroot()
for elem in root.iter():
print(elem.tag, elem.attrib)
# config {}
# database {'host': 'localhost', 'port': '5432'}
# cache {'enabled': 'true', 'ttl': '300'}
iter() traverses the entire tree depth-first. iterfind() limits traversal to elements matching a tag or path.
Element Objects
Each element has:
tag— the element name (a string)attrib— a dict of attributestext— the text content between the opening and closing tagstail— the text after the closing tag, up to the next siblingchildren— sub-elements accessed by index or iteration
Accessing attributes and text
root = ET.fromstring('<product name="Widget" price="9.99" quantity="100"/>')
root.attrib['name'] # => 'Widget'
root.attrib.get('price') # => '9.99'
root.attrib.get('weight') # => None (missing attribute)
root.attrib.get('weight', 'N/A') # => 'N/A' (with default)
root.text # => None (no text content here)
Navigating the tree
config = ET.fromstring("""<config>
<database host="localhost" port="5432"/>
</config>""")
db = config[0]
print(db.tag) # => 'database'
print(db.attrib['host']) # => 'localhost'
# Children iteration
for child in config:
print(child.tag, child.attrib)
Building XML
Creating elements
root = ET.Element("person")
root.attrib["id"] = "p001"
name = ET.SubElement(root, "name")
name.text = "Alice"
age = ET.SubElement(root, "age")
age.text = "30"
# Pretty print with indentation
ET.indent(root)
ET.tostring(root, encoding="unicode")
# <person id="p001">
# <name>Alice</name>
# <age>30</age>
# </person>
SubElement() creates a child element and appends it in one step. Set .text on the element for its text content.
Writing to a file
tree = ET.ElementTree(root)
tree.write("output.xml", encoding="utf-8", xml_declaration=True)
The file is written with UTF-8 encoding by default.
XPath Support
ElementTree supports a subset of XPath for finding elements:
root = ET.parse("inventory.xml").getroot()
# Find all <item> elements anywhere in the tree
items = root.findall(".//item")
# Find direct children with a specific tag
products = root.findall("product")
# Find first matching element
first = root.find(".//item[@id='001']")
| Expression | Meaning |
|---|---|
tag | Direct child with that tag |
. | Current element |
.. | Parent element |
//tag | Any descendant with that tag |
[@attr] | Element with that attribute |
[@attr='value'] | Element with exact attribute match |
Serialization
To string
root = ET.Element("config")
ET.SubElement(root, "setting").text = "value"
ET.tostring(root) # bytes
ET.tostring(root, encoding="unicode") # string
To file
tree = ET.ElementTree(root)
tree.write("config.xml", encoding="utf-8")
Common Use Cases
Processing RSS feeds
import xml.etree.ElementTree as ET
rss_xml = """<rss version="2.0">
<channel>
<item><title>First Post</title><link>https://example.com/1</link></item>
<item><title>Second Post</title><link>https://example.com/2</link></item>
</channel>
</rss>"""
root = ET.fromstring(rss_xml)
for item in root.findall(".//item"):
title = item.find("title").text
link = item.find("link").text
print(f"{title}: {link}")
Reading configuration files
import xml.etree.ElementTree as ET
config = ET.parse("app_config.xml").getroot()
for section in config:
print(f"Section: {section.tag}")
for key, val in section.attrib.items():
print(f" {key} = {val}")
Modifying XML and writing back
tree = ET.parse("data.xml")
root = tree.getroot()
# Update an attribute
for elem in root.iter("database"):
elem.attrib["port"] = "5433"
# Add a new child
new_elem = ET.SubElement(root.find("settings"), "option")
new_elem.text = "enabled"
# Write back
tree.write("data_modified.xml", encoding="utf-8")
Gotchas
Attribute order is not preserved. XML attributes are stored in a dict, which has no guaranteed order in Python versions before 3.7. If you need a specific output order, use a different library.
Namespaces can confuse find. When XML uses namespaces, bare tag names may not match:
<root xmlns:ns="http://example.com">
<ns:item>text</ns:item>
</root>
Use the namespace URI in your searches:
ns = {"ex": "http://example.com"}
root.find("ex:item", ns) # finds the element
Register namespace prefixes to keep output clean:
ET.register_namespace("ex", "http://example.com")
Not safe for untrusted input. ElementTree is vulnerable to billion laugh attacks and external entity attacks. For untrusted XML, use the defusedxml library instead, which disables these features.
tostring() strips the XML declaration by default. Pass xml_declaration=True to include it:
ET.tostring(root, encoding="utf-8", xml_declaration=True)
See Also
- /reference/modules/json-module/ — parse JSON, which is simpler than XML for most data exchange
- /guides/python-dotenv/ — simpler configuration file format for key-value pairs
- /tutorials/python-and-data/working-with-apis/ — parse XML responses from HTTP APIs