Parsing XML in code: DOM, SAX and pull parsers
Tree versus streaming models, how a SAX handler works, why a pull parser is usually the better streaming choice, and how to pick by document size.
Three ways to read the same document
| Model | Memory | Random access | Style | Best for |
|---|---|---|---|---|
| DOM | Whole tree, several times the file size | Yes | Object graph | Small documents, repeated queries |
| SAX | Constant | No | Callbacks you write | Very large documents, single pass |
| StAX / pull | Constant | No, forward only | A loop you control | Large documents, readable code |
| XPath over DOM | Whole tree | Yes | Query | Small to medium documents |
| Streaming XPath | Constant | Limited | Query with a push model | Large documents, selective extraction |
# DOM: everything in memory, then query
import xml.etree.ElementTree as ET
tree = ET.parse("orders.xml")
root = tree.getroot()
for order in root.findall("order"):
print(order.get("id"), order.findtext("total"))
# a 200 MB document needs well over 1 GB of heap this wayStreaming without losing your place
# iterparse: a pull parser, constant memory
import xml.etree.ElementTree as ET
def stream_orders(path):
# clear elements as they close so the tree does not grow
for event, elem in ET.iterparse(path, events=("end",)):
if elem.tag == "order":
yield {
"id": elem.get("id"),
"total": elem.findtext("total"),
"items": len(elem.findall("items/item")),
}
elem.clear()
for order in stream_orders("orders.xml"):
print(order)elem.clear()is what keeps memory constant; without ititerparsestill builds the whole tree.- Clearing a node removes its children, so read everything you need before clearing.
- Namespace handling differs between libraries; with ElementTree, tags come back as
{uri}localstrings. - A pull parser lets you stop early, which a push parser cannot do without raising an exception.
// SAX: you implement the handler and the parser drives it
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); // XXE off
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
XMLStreamReader r = factory.createXMLStreamReader(new FileInputStream("orders.xml"));
while (r.hasNext()) {
int event = r.next();
if (event == XMLStreamConstants.START_ELEMENT && "order".equals(r.getLocalName())) {
System.out.println(r.getAttributeValue(null, "id"));
}
}
r.close();Choosing by document size
| Document size | Recommendation | Why |
|---|---|---|
| Under a few MB | DOM or ElementTree | Simplest code, random access |
| A few MB to tens of MB | Either, but measure | DOM is convenient; watch the heap |
| Tens of MB and up | Pull parser | Constant memory, sequential access |
| Unknown or unbounded | Pull parser with limits | Protects against a hostile input size |
| Need XPath over a huge file | Streaming XPath or a database | DOM will not fit |
| Need a whole document validated then queried | DOM after validation | Validation needs a schema-aware parser anyway |
⚠️
Never parse untrusted XML with a default-configured parser. Disable DTD processing and external entities first, and cap the input size and the depth. A ten-line document can exhaust memory or read a local file when those settings are wrong.
FAQ
Why is my iterparse still using gigabytes?
You are not clearing elements, or you are holding references to them. Clear each finished element and avoid building a list of results in memory.
Is SAX still relevant?
It remains the lowest-overhead option in many libraries and it is what a pull parser is built on. For new code a pull parser is usually easier to reason about.
Related
XML in Python, Java and JavaScript XML security: XXE and entity expansion
Last refreshed 2026-09-18.