Python pulldom: Partial DOM for XML

Published on: August 23, 2026
Reading time: 5 minutes
A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.

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()}")

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A laptop screen showing a code editor with visible programming code in a dimly lit environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xml.sax: Process XML Events

    Learn Python xml.sax to process XML as events with low memory use, namespaces, handlers, limits, and secure entity handling.

    Ler mais

    Tempo de leitura: 4 minutos
    22/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python minidom: Manipulate XML with DOM

    Learn Python xml.dom.minidom to read, navigate, create, and serialize XML with DOM nodes, namespaces, memory control, and security.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    A solitary tree with sunlight filtering through the leaves on a grassy hill under a blue sky.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ElementTree: Read and Modify XML in Python

    Learn Python ElementTree to read, search, modify, and generate XML with namespaces, incremental parsing, limits, and security.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python poplib: Read Email with POP3

    Learn Python poplib to access POP3 with TLS, list and download messages, use UIDL, enforce limits, and avoid accidental deletion.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python imaplib: Read Email with IMAP

    Learn Python imaplib to access IMAP mailboxes with TLS, search by UID, read without marking messages, manage flags, and avoid

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Extreme close-up of computer code displaying various programming terms and elements.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ftplib: Secure FTP and FTPS

    Learn Python ftplib to list, download, and upload files over FTP or FTPS with TLS, timeouts, limits, resume, and clear

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026