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

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026