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

ModelMemoryRandom accessStyleBest for
DOMWhole tree, several times the file sizeYesObject graphSmall documents, repeated queries
SAXConstantNoCallbacks you writeVery large documents, single pass
StAX / pullConstantNo, forward onlyA loop you controlLarge documents, readable code
XPath over DOMWhole treeYesQuerySmall to medium documents
Streaming XPathConstantLimitedQuery with a push modelLarge 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 way

Streaming 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 it iterparse still 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}local strings.
  • 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 sizeRecommendationWhy
Under a few MBDOM or ElementTreeSimplest code, random access
A few MB to tens of MBEither, but measureDOM is convenient; watch the heap
Tens of MB and upPull parserConstant memory, sequential access
Unknown or unboundedPull parser with limitsProtects against a hostile input size
Need XPath over a huge fileStreaming XPath or a databaseDOM will not fit
Need a whole document validated then queriedDOM after validationValidation 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.

XML in Python, Java and JavaScript XML security: XXE and entity expansion

Last refreshed 2026-09-18.