The xml.dom.pulldom module provides a middle ground for XML processing in Python. It combines a pull-based event stream, similar in spirit to SAX, with the ability to build complete DOM subtrees only when they are needed. This is useful when a document is too large to load entirely into memory but selected sections still require DOM navigation, attributes, children, and serialization.
Instead of building the whole tree immediately, the program iterates over events such as element starts, text, comments, and document completion. When it finds a relevant element, it calls expandNode() to materialize only that subtree. The result balances the low memory footprint of event-driven parsing with the convenience of DOM nodes.
When pulldom is useful
pulldom fits XML files with many repeated records, including catalogs, financial exports, logs, feeds, reports, and legacy integrations. A document may contain thousands of elements while an application needs only records with a certain status, price, identifier, or date.
When the complete document comfortably fits in memory and free navigation is required, see the guide to Python minidom. When you only need events and extracted values without DOM fragments, Python xml.sax is more direct. For many structural transformations, Python ElementTree offers a simpler API.
A first example
from xml.dom import pulldom
events = pulldom.parse("catalog.xml")
for event, node in events:
if event == pulldom.START_ELEMENT and node.tagName == "product":
events.expandNode(node)
print(node.toxml())
Before expandNode(), the start node does not contain all materialized children. After expansion, it behaves like a minidom element and supports methods such as getAttribute(), getElementsByTagName(), and toxml().
Filter before expanding
The main advantage appears when attributes available on the start event can decide whether a subtree is worth expanding.
from decimal import Decimal
from xml.dom import pulldom
events = pulldom.parse("catalog.xml")
for event, node in events:
if event != pulldom.START_ELEMENT:
continue
if node.tagName != "product":
continue
try:
price = Decimal(node.getAttribute("price"))
except Exception:
continue
if price >= Decimal("100.00"):
events.expandNode(node)
print(node.toxml())
This avoids building DOM objects for inexpensive products. Large external documents still need limits for byte size, depth, number of elements, attributes, and expanded fragments.
Available events
The stream may produce START_DOCUMENT, END_DOCUMENT, START_ELEMENT, END_ELEMENT, CHARACTERS, COMMENT, PROCESSING_INSTRUCTION, and IGNORABLE_WHITESPACE. The node type varies with the event and may be a document, element, or text node.
Do not assume all text arrives in one event. Event parsers may split contiguous character data into several pieces. If text is collected without expanding a subtree, append fragments to a list and join them when the matching end event arrives.
Track context with a stack
The event stream is flat. When the path of the current element matters, maintain a stack of names.
from xml.dom import pulldom
events = pulldom.parse("data.xml")
stack = []
for event, node in events:
if event == pulldom.START_ELEMENT:
stack.append(node.tagName)
path = "/".join(stack)
if path == "catalog/section/product":
events.expandNode(node)
print(node.getAttribute("id"))
stack.pop()
elif event == pulldom.END_ELEMENT and stack:
stack.pop()
Because expandNode() consumes the internal events of that element, keep stack updates consistent with your strategy and validate them with small fixtures.
Files, streams, and strings
parse() accepts a filename or a file-like object. parseString() accepts XML already held in memory. For network data, download with explicit limits and consider storing it in a temporary file before parsing. The guide to Python urllib.request covers bounded downloads, while Python tempfile covers safer temporary storage.
from io import BytesIO
from xml.dom import pulldom
xml_bytes = b"<root><item id='1'/></root>"
events = pulldom.parse(BytesIO(xml_bytes))
Do not accept an unlimited client string and pass it directly to parseString(). Enforce the limit before allocating the complete buffer.
Namespaces
pulldom.parse() enables namespace support on the supplied parser. With namespaced XML, inspect namespaceURI, localName, and tagName carefully. Prefixes may change while the namespace meaning remains identical.
from xml.dom import pulldom
events = pulldom.parse("feed.xml")
for event, node in events:
if event == pulldom.START_ELEMENT:
if node.namespaceURI == "urn:example:catalog" and node.localName == "product":
events.expandNode(node)
print(node.toxml())
Comparing only a source prefix such as cat:product makes the application fragile. Prefer the namespace URI and local name.
External entity security
The official documentation warns that unauthenticated XML may be dangerous. Since Python 3.7.1, the default SAX parser used by this module no longer processes general external entities. Do not re-enable the feature for user-provided documents. External entities may trigger local file reads, unexpected network requests, or expansion attacks.
If a custom parser is necessary, install an EntityResolver that rejects external resources and keep external entity features disabled. Incremental processing does not make hostile XML safe by itself.
Depth and volume limits
Set explicit limits for input bytes, event count, stack depth, attribute length, number of expanded nodes, and serialized output size. A small source file can still contain extreme nesting or large text values.
MAX_EVENTS = 1_000_000
MAX_DEPTH = 100
event_count = 0
stack = []
for event, node in events:
event_count += 1
if event_count > MAX_EVENTS:
raise ValueError("XML event limit exceeded")
if event == pulldom.START_ELEMENT:
stack.append(node.tagName)
if len(stack) > MAX_DEPTH:
raise ValueError("XML nesting is too deep")
elif event == pulldom.END_ELEMENT and stack:
stack.pop()
Expand, extract, and release
An expanded subtree remains a graph of DOM objects while references exist. Process it, extract only the required values, and discard the node. Avoid keeping every expanded node in a list for a large file.
records = []
for event, node in events:
if event == pulldom.START_ELEMENT and node.tagName == "product":
events.expandNode(node)
records.append({
"id": node.getAttribute("id"),
"xml": node.toxml(),
})
node.unlink()
unlink() can release internal references sooner, but call it only after the fragment is no longer needed.
Serialization is not sanitization
toxml() produces XML syntax; it does not make values trustworthy. Extracted content still requires validation before use in HTML, SQL, shell commands, paths, or logs. The guide to Python html.entities explains why decoding and sanitization are different operations.
Error handling
Catch parsing failures at a clear boundary, record the origin and position without logging secrets, and reject incomplete documents. Critical integrations should not silently repair malformed XML.
from xml.dom import pulldom
from xml.sax import SAXParseException
try:
events = pulldom.parse("input.xml")
for event, node in events:
pass
except SAXParseException as exc:
print(f"Invalid XML at line {exc.getLineNumber()}")
Recommended tests
Test empty files, alternate prefixes, missing attributes, fragmented text, excessive depth, disabled external entities, truncated files, different encodings, and multiple target elements. For small fixtures, compare output against ElementTree or minidom.
Choosing another API
Use ElementTree.iterparse() when a lightweight tree and incremental events are enough. Use SAX when no DOM navigation is needed. Use minidom when the complete document is small and DOM compatibility is important. Choose pulldom when only selected fragments need complete DOM behavior.
Conclusion
xml.dom.pulldom lets an application scan XML as events and selectively expand relevant elements. This reduces memory use while preserving DOM convenience for the fragments that matter.
Filter before expansion, keep external entities disabled, enforce limits, and release references promptly. Consult the official pulldom documentation and Python’s XML security guidance.







