Parse and Build Small XML Documents
Use ElementTree to parse trusted small XML and create escaped XML elements.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Parse and Build Small XML Documents
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
XML stores information in nested elements, with attributes and text attached to those elements. The Python standard library includes `xml.etree.ElementTree` for ordinary XML work. A frequent beginner mistake is to search only direct children when the desired element is nested further down. That produces `None`, and code that immediately reads an attribute can fail. In this lesson we will reproduce that mistake, inspect the tree's actual shape, then write a small parser that validates each item before building a summary. Finally, we will update the tree and serialize it back to XML.
- 00:40
All examples use a short trusted string kept inside the lesson; there is no network request or external file. If your application processes untrusted XML, review the standard library's XML security guidance and consider stricter limits or a hardened parser. For now, our goal is to understand paths, attributes, text, and the point at which a missing element must be handled. Watch the actual Python run at each step. The item is visibly present in the string, yet `root.find('item')` returns `None`. The call checks an `item` directly under the root, but the direct child is `group`.
- 01:20
The last line confirms that shape. This is not a parsing failure: the XML was parsed, and the root has the expected name. The failure is a mismatch between our path and the tree. If the next line had been `found.attrib['sku']`, Python would raise an attribute error because `found` is `None`. Before making a parser more complicated, print or inspect the tags along the path. Decide whether the schema guarantees exactly one group, whether there may be several, and whether an item can be deeper. In a robust parser, a missing element should become a clear validation result rather than an unrelated exception later.
- 02:00
This small wrong-path example helps us separate the data's structure from the way its XML text happens to be indented or written on one line. The path `.//item` selects matching descendant elements below the current element. Both groups contribute one item, so the count is two. `item.get('sku')` reads an attribute; `item.text` reads the text directly inside the tag. Those are different places in the XML data model. Text can be `None`, especially for an empty element, so we normalize it to an empty string before stripping whitespace. This parser still trusts the values. It does not yet require a nonempty SKU or a name.
- 02:43
A broad descendant path is convenient, but in a larger document it might also find an `item` in an unrelated section. If the document schema is stable, a narrower path such as `./group/item` is often clearer. We choose a path based on the structure we expect, then test it against multiple groups and missing values. Avoid attempting to parse XML by splitting on angle brackets: attributes, escaping, namespaces, and nested elements quickly break string tricks. The function first checks that the root is the document type we expect. Then it uses the explicit path `./group/item` to select items directly inside groups.
- 03:24
Every selected item must have a nonempty SKU and nonblank name. If either is missing, the function raises a useful error at the boundary where it understands the document. That is easier to debug than a later `KeyError` or `AttributeError` in unrelated code. Our result is a list of simple tuples because that is enough for the example; a larger application might use dataclasses and include location information in each validation error. Notice one limit: a catalog with no groups currently returns an empty list. Whether that is valid is a product decision, not something `ElementTree` can decide.
- 04:04
Add an explicit nonempty check if your schema requires at least one item. Similarly, XML namespaces change how tags are named; inspect and specify them when your input uses namespaces rather than assuming a bare `item` tag. We create a new child element under `group`, set its text, and serialize the tree to an XML string. Parsing that string again gives us two items. The assertion checks the data we care about, rather than comparing the exact formatting of the serialized text. XML serializers may choose different whitespace or attribute presentation, while the tree's meaning stays the same.
- 04:45
The earlier validator should still be used if a serialized or incoming document can contain missing fields. You can test that by deleting the new item's SKU before calling the validation function from the previous chapter. Try another path mistake as well: change `./group/item` to `./item`, predict the empty result, and then run it. The lesson's core skill is to reason from the tree: choose the right path, distinguish attributes from text, validate what your application requires, and test a round trip after an edit. Keep this local fixture small enough to inspect. For unfamiliar or untrusted XML formats, check size limits and the XML security
- 05:27
guidance before accepting input from other people.
Understand the concept
XML stores structured data as elements and attributes. Splitting on angle brackets is not a parser: text can contain escaped characters, elements can nest, and whitespace can vary. xml.etree.ElementTree parses a small trusted fixture into elements whose tags, attributes, and text can be inspected. A parser can report malformed syntax as ParseError.
In a catalog, each direct book element has a year attribute and a title child. findall('./book') visits those direct children; get('year') reads the attribute; findtext('title') returns text or None. Convert the year only after checking that it exists and is a valid integer. Reject a wrong root or blank title explicitly so a malformed record does not silently enter a report.
When creating XML, build nodes with Element and SubElement and let ElementTree escape text such as A & B into A & B. Concatenating user text into '<title>' strings can produce broken XML or unintended markup. Remember that ElementTree is an XML tool, not an HTML sanitizer or a policy for untrusted input.
This browser lesson uses short, known XML strings. Python's XML documentation warns that some XML libraries and parsers are unsuitable for untrusted or very large XML; consult those security notes before processing external files. The checked task catches malformed syntax as ValueError, validates a simple catalog schema, and returns (title, year) tuples in document order.
- ElementTree parses nested XML
- Attributes and child text are different
- ElementTree escapes text when writing
- Validate syntax and application schema separately
See it step by step
Read the code, predict the output, then compare it with the result.
01. Read escaped text as a real character
import xml.etree.ElementTree as ET
root = ET.fromstring('<catalog><book year="2024"><title>A & B</title></book></catalog>')
book = root.find('./book')
print(book.get('year'), book.findtext('title'))2024 A & BThe parser decodes the entity into text while preserving the attribute separately.
02. Let ElementTree escape output
import xml.etree.ElementTree as ET
root = ET.Element('catalog')
book = ET.SubElement(root, 'book', year='2024')
ET.SubElement(book, 'title').text = 'A & B'
print(ET.tostring(root, encoding='unicode'))<catalog><book year="2024"><title>A & B</title></book></catalog>ElementTree writes a valid escaped ampersand rather than raw broken XML.
03. Distinguish malformed syntax from a missing field
import xml.etree.ElementTree as ET
for xml in ('<catalog>', '<catalog><book year="2024"/></catalog>'):
try:
root = ET.fromstring(xml)
print(root.find('./book/title').text if root.find('./book/title') is not None else 'missing title')
except ET.ParseError:
print('invalid XML syntax')invalid XML syntax
missing titleSyntax errors and application-schema errors need different handling.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Parse escaped text and an attribute separately
The first example parses a small catalog. The year is an attribute of book, while the title is a child element. ElementTree decodes & into the text A & B, then the run prints both fields. Splitting raw XML on angle brackets or ampersands would lose that structure.
Treat XML syntax and your application's expected schema as separate layers. A parser can produce an element tree from valid XML even if a book lacks the title the catalog requires. Check the root tag, required attributes, and child text explicitly before converting the year. These fixtures are small and trusted; large or adversarial XML needs additional security and resource review.
import xml.etree.ElementTree as ET
root = ET.fromstring('<catalog><book year="2024"><title>A & B</title></book></catalog>')
book = root.find('./book')
print(book.get('year'), book.findtext('title'))2024 A & B- Parse the catalog with ElementTree.
- Read the year attribute and title child.
- Validate fields before converting them to application values.
Let the XML library escape output
The second run creates elements and assigns a title containing an ampersand. ElementTree serializes it as A & B inside the title node. The escaped output remains well-formed XML, while direct string concatenation could produce broken markup or change its structure.
Use Element and SubElement to build the intended tree, then set .text for content. This is an XML technique, not a substitute for HTML sanitization or a policy for processing external documents. Tests should parse the serialized output again and compare the title text, rather than assuming a particular whitespace style from one serializer version.
import xml.etree.ElementTree as ET
root = ET.Element('catalog')
book = ET.SubElement(root, 'book', year='2024')
ET.SubElement(book, 'title').text = 'A & B'
print(ET.tostring(root, encoding='unicode'))<catalog><book year="2024"><title>A & B</title></book></catalog>- Create the root and child nodes explicitly.
- Assign special characters as text rather than markup.
- Serialize and parse back to verify the value.
Distinguish syntax failure from missing data
The last run tries two different broken inputs. An unclosed catalog raises ParseError and prints invalid XML syntax. A well-formed book with no title reaches the tree but prints missing title. Treating both as the same parsing failure would hide whether the bytes or the domain schema needs repair.
The checked catalog reader converts a malformed document into a clear ValueError, then separately validates each book's required title and year. Test a wrong root, absent title, and nonnumeric year as well as the valid fixture. For XML from outside a trusted source, consult Python's XML security guidance and impose size limits before adopting this small reader.
import xml.etree.ElementTree as ET
for xml in ('<catalog>', '<catalog><book year="2024"/></catalog>'):
try:
root = ET.fromstring(xml)
print(root.find('./book/title').text if root.find('./book/title') is not None else 'missing title')
except ET.ParseError:
print('invalid XML syntax')invalid XML syntax
missing title- Feed a malformed XML string and catch ParseError.
- Feed valid syntax with a missing required child.
- Report the correct layer of failure to the caller.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Catch ET.ParseError and raise ValueError. Require root.tag == 'catalog'. For each root.findall('./book'), use get('year') and findtext('title'), validate, then append a tuple.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Building XML by concatenation
xml = '<title>' + title + '</title>'ET.SubElement(book, 'title').text = titleWhat happens: A title containing & produces invalid XML.
Assign text and let ElementTree escape it.
Assuming optional fields exist
year = int(book.get('year'))if book.get('year') is None: raise ValueError('missing year')What happens: A missing year fails with a confusing conversion error.
Validate the catalog schema before conversion.