Parsing Nested XML and Attributes with Python xmltodict
python xmltodict nested xml and attributes: Learn how to use xmltodict to parse nested XML and handle attributes, including repeated elements, namespaces, and converti...
When working with XML in Python, the standard library's xml.etree.ElementTree is verbose for nested structures. The xmltodict library offers a simpler mapping: XML becomes nested dictionaries and lists, with attributes prefixed by @. This article focuses on python xmltodict nested xml and attributes — how to parse complex XML, access attributes, handle repeated elements, and convert back to XML without losing data.
Parsing XML into Python Dictionaries with xmltodict
The core function is xmltodict.parse(). It takes an XML string or file-like object and returns an OrderedDict. For a simple element without attributes, the mapping is straightforward:
import xmltodict xml_string = """ <book> <title>Python for Data Analysis</title> <author>Wes McKinney</author> </book> """ data = xmltodict.parse(xml_string) print(data) # OrderedDict([('book', OrderedDict([('title', 'Python for Data Analysis'), ('author', 'Wes McKinney')]))])
The root element becomes the key, and child elements become keys in the nested dictionary. Text content is stored as a string. This mapping makes XML feel like JSON, which is why many developers reach for xmltodict.
How xmltodict Represents XML Attributes
Attributes are distinguished from child elements by an @ prefix. For example:
xml_string = """ <book id="123" lang="en"> <title>Python for Data Analysis</title> </book> """ data = xmltodict.parse(xml_string) print(data['book']['@id']) # '123' print(data['book']['@lang']) # 'en'
Attributes are stored as keys with @ at the same level as child elements. This convention is consistent across the library. When converting back to XML, the @ prefix tells unparse() that the key is an attribute.
Text content mixed with attributes or child elements is stored under the key #text. For instance:
xml_string = """ <message id="1">Hello world</message> """ data = xmltodict.parse(xml_string) print(data['message']) # OrderedDict([('@id', '1'), ('#text', 'Hello world')])
This #text key is essential when an element has both attributes and text content.
Handling Nested XML Structures
Nested elements become nested dictionaries naturally. Consider a deeper structure:
xml_string = """ <company> <employee> <name>Alice</name> <department>Engineering</department> </employee> </company> """ data = xmltodict.parse(xml_string) print(data['company']['employee']['name']) # 'Alice'
Accessing nested data is intuitive. However, when an element repeats, xmltodict stores it as a list. This is a common source of confusion because the structure changes based on the number of occurrences.
Managing Repeated Elements and Lists
If an XML element appears more than once under the same parent, xmltodict groups them into a list. For example:
xml_string = """ <company> <employee> <name>Alice</name> </employee> <employee> <name>Bob</name> </employee> </company> """ data = xmltodict.parse(xml_string) employees = data['company']['employee'] print(type(employees)) # <class 'list'> print(employees[0]['name']) # 'Alice'
But if only one <employee> exists, data['company']['employee'] is a dictionary, not a list. This inconsistency forces you to write defensive code. A common pattern is to normalize with a helper:
def as_list(value): if value is None: return [] if isinstance(value, list): return value return [value]
Then you can always iterate over as_list(data['company']['employee']). This is a practical workaround for the dynamic typing that xmltodict introduces.
Converting Dictionaries Back to XML with unparse
The reverse operation uses xmltodict.unparse(). It accepts a dictionary and returns an XML string. Attributes must use the @ prefix, and text content uses #text:
import xmltodict data = { 'book': { '@id': '123', 'title': 'Python for Data Analysis', 'author': 'Wes McKinney' } } xml_string = xmltodict.unparse(data, pretty=True) print(xml_string) # <book id="123"> # <title>Python for Data Analysis</title> # <author>Wes McKinney</author> # </book>
When a value is a list, unparse generates repeated elements. This makes round-tripping possible, but you must ensure the dictionary structure matches the @ and #text conventions. If you omit the @ prefix on an attribute, it will be treated as a child element instead.
Common Pitfalls with Namespaces and Mixed Content
XML namespaces are a major source of friction. By default, xmltodict includes the namespace URI in the key, often as {http://example.com}tag. This makes access verbose. You can disable namespace processing with process_namespaces=False, but then you lose the ability to distinguish same-named elements from different namespaces. A better approach is to use the namespaces parameter to strip prefixes:
xml_string = """ <root xmlns:h="http://www.w3.org/TR/html4/"> <h:table> <h:tr><h:td>Cell</h:td></h:tr> </h:table> </root> """ data = xmltodict.parse(xml_string, process_namespaces=True, namespaces={'http://www.w3.org/TR/html4/': None}) print(data['root']['table']) # Namespace prefix stripped
Mixed content — an element with both text and child elements — is rare but possible. xmltodict will place text under #text and children as separate keys. This can make traversing the dictionary awkward because the order is lost. If your XML relies heavily on mixed content, xmltodict may not be the right tool; consider lxml instead.
Performance and Memory Considerations
xmltodict builds a complete in-memory representation of the XML document. For large files, this can consume significant memory and CPU. Unlike ElementTree's iterparse, xmltodict does not support streaming. If you need to process a multi-gigabyte XML file, you should use a streaming parser. For typical configuration files or API responses, the convenience outweighs the overhead.
Another performance nuance is that xmltodict returns OrderedDict objects. While they preserve order, they have slightly higher memory overhead than plain dict. If order is not important, you can convert to standard dicts, but that loses the original XML order. In practice, most developers accept this tradeoff.
When converting back to XML, unparse serializes the entire dictionary tree. This is also an in-memory operation. For large dictionaries, consider whether you truly need to produce XML or if JSON is a better interchange format.