Python xml.sax: Process XML Events

Published on: August 22, 2026
Reading time: 4 minutes
A laptop screen showing a code editor with visible programming code in a dimly lit environment.

The xml.sax package implements the Simple API for XML, an event-driven parsing model. Instead of building a complete tree, the parser reads a document and invokes methods when it encounters element starts and ends, character data, namespace mappings, processing instructions, and errors.

SAX is useful for large files, transformation pipelines, imports, and validation workflows that do not need the whole XML document in memory. The trade-off is that the application must maintain state, accumulate fragmented text, and decide what to do during each event.

Your first SAX handler

import xml.sax
from xml.sax.handler import ContentHandler

class CatalogHandler(ContentHandler):
    def startElement(self, name, attrs):
        print("start", name, dict(attrs))

    def characters(self, content):
        if content.strip():
            print("text", repr(content))

    def endElement(self, name):
        print("end", name)

xml.sax.parse("catalog.xml", CatalogHandler())

parse() creates a parser, connects the handler, and processes a filename or stream. All application work happens in callbacks; no tree is returned.

characters may be called several times

The parser does not guarantee that contiguous text arrives in one callback. Internal buffering can split a single field into multiple pieces.

class ProductsHandler(ContentHandler):
    def __init__(self):
        super().__init__()
        self.current_tag = None
        self.buffer = []

    def startElement(self, name, attrs):
        self.current_tag = name
        self.buffer.clear()

    def characters(self, content):
        if self.current_tag in {"name", "price"}:
            self.buffer.append(content)

    def endElement(self, name):
        if name in {"name", "price"}:
            value = "".join(self.buffer).strip()
            print(name, value)
        self.current_tag = None
        self.buffer.clear()

Accumulate data until the closing event. Never treat each characters() call as a complete field.

State and an element stack

For nested structures, maintain a stack that represents the current path.

class PathHandler(ContentHandler):
    def __init__(self):
        self.stack = []

    def startElement(self, name, attrs):
        self.stack.append(name)
        print("/".join(self.stack))

    def endElement(self, name):
        if not self.stack or self.stack[-1] != name:
            raise ValueError("inconsistent parser state")
        self.stack.pop()

Set a maximum depth so deeply nested hostile XML cannot consume unbounded resources.

Configuring a parser

import xml.sax
from xml.sax.handler import feature_namespaces

parser = xml.sax.make_parser()
parser.setFeature(feature_namespaces, True)
parser.setContentHandler(MyHandler())
parser.parse("data.xml")

make_parser() returns an XMLReader. Configure features before parsing. Changing a feature while parsing can raise SAXNotSupportedException.

Namespaces

With namespace processing enabled, implement startElementNS() and endElementNS(). The name arrives as a (URI, localname) tuple.

class AtomHandler(ContentHandler):
    ATOM = "http://www.w3.org/2005/Atom"

    def startElementNS(self, name, qname, attrs):
        uri, local = name
        if uri == self.ATOM and local == "entry":
            print("new entry")

qname may be None unless namespace-prefix reporting is enabled. Use the URI and local name for domain logic.

Prefix mapping events

startPrefixMapping() and endPrefixMapping() report prefix scopes. Their order is not guaranteed to form a simple nested stack relative to one another. Maintain a scope-aware mapping if QNames inside text or attribute values must be interpreted.

Attributes

The parser may reuse the attrs object. If data must survive beyond the callback, make a copy.

def startElement(self, name, attrs):
    attributes = dict(attrs.items())
    self.queue.append((name, attributes))

In namespace mode, use AttributesNS and keys based on URI and local name.

Locator for line and column

class LocatedHandler(ContentHandler):
    def setDocumentLocator(self, locator):
        self.locator = locator

    def startElement(self, name, attrs):
        if name == "product" and "id" not in attrs:
            line = self.locator.getLineNumber()
            column = self.locator.getColumnNumber()
            raise ValueError(
                f"product without id at {line}:{column}"
            )

The locator is accurate during callbacks. Copy line and column immediately if the information is needed later.

Error handling

An ErrorHandler receives warnings, recoverable errors, and fatal errors.

from xml.sax.handler import ErrorHandler

class Errors(ErrorHandler):
    def warning(self, exception):
        print("warning", exception)

    def error(self, exception):
        raise exception

    def fatalError(self, exception):
        raise exception

Without a custom error handler, parse errors usually raise SAXParseException. Do not continue using partially processed results after a recoverable error unless the application has a very specific policy.

Blocking external entities

General external entities are disabled by default in current Python releases. Do not re-enable feature_external_ges for user-supplied XML.

from xml.sax.handler import EntityResolver, feature_external_ges
from xml.sax.xmlreader import InputSource
import io

class BlockingResolver(EntityResolver):
    def resolveEntity(self, publicId, systemId):
        source = InputSource()
        source.setCharacterStream(io.StringIO(""))
        return source

parser.setFeature(feature_external_ges, False)
parser.setEntityResolver(BlockingResolver())

External entities can enable local-file reads or outbound network requests. Evaluate the threat model before changing the secure default.

InputSource

InputSource can specify a byte stream, character stream, encoding, and system identifier.

from xml.sax.xmlreader import InputSource

source = InputSource()
source.setByteStream(binary_file)
source.setSystemId("controlled-input.xml")
parser.parse(source)

Do not use an untrusted remote system ID. It can influence resource resolution and diagnostics.

Byte, element, and depth limits

SAX reduces memory use but does not eliminate CPU, depth, or giant-text attacks. Count elements, attributes, depth, field length, and total input bytes.

class LimitedHandler(ContentHandler):
    def __init__(self, max_elements=100_000):
        self.max_elements = max_elements
        self.elements = 0
        self.depth = 0

    def startElement(self, name, attrs):
        self.elements += 1
        self.depth += 1
        if self.elements > self.max_elements:
            raise ValueError("too many elements")
        if self.depth > 100:
            raise ValueError("XML is too deeply nested")

    def endElement(self, name):
        self.depth -= 1

Streaming records

A common pattern keeps only the current record in memory.

class RecordsHandler(ContentHandler):
    def __init__(self, destination):
        self.destination = destination
        self.record = None
        self.field = None
        self.buffer = []

    def startElement(self, name, attrs):
        if name == "record":
            self.record = {"id": attrs.get("id")}
        elif self.record is not None:
            self.field = name
            self.buffer = []

    def characters(self, content):
        if self.field is not None:
            self.buffer.append(content)

    def endElement(self, name):
        if self.record is None:
            return
        if name == "record":
            self.destination(self.record)
            self.record = None
        elif name == self.field:
            self.record[name] = "".join(self.buffer).strip()
            self.field = None
            self.buffer = []

Validate and persist each record. Use transactions and idempotency when the same input can be retried.

Stopping early

Raise a custom exception when the desired value is found or a limit is reached. Ensure the external stream is closed in a finally block.

LexicalHandler

An optional LexicalHandler receives comments, DTD boundaries, and CDATA boundaries. Configure it with property_lexical_handler. Not every parser supports every property, so handle SAXNotRecognizedException and SAXNotSupportedException.

DTDHandler

A DTDHandler receives notation and unparsed-entity declarations. Most applications do not need it. Do not enable external DTD processing or validation merely to obtain additional events from untrusted input.

Generating XML with saxutils

xml.sax.saxutils includes escape(), quoteattr(), and XMLGenerator.

from xml.sax.saxutils import XMLGenerator

with open("output.xml", "w", encoding="utf-8") as file:
    generator = XMLGenerator(file, encoding="utf-8")
    generator.startDocument()
    generator.startElement("status", {})
    generator.characters("ok & validated")
    generator.endElement("status")
    generator.endDocument()

The generator escapes character data correctly. Still validate dynamically chosen tag and attribute names.

SAX, ElementTree, or minidom

SAX is ideal for streaming and controlled memory. ElementTree is simpler when you need to query and modify a tree. Minidom offers a complete DOM node model but keeps many objects in memory.

Read Python ElementTree and Python minidom.

Encoding

Prefer supplying bytes so the XML declaration determines the encoding. If text is decoded before parsing, ensure that the chosen codec matches the document. For codec issues, see Python codecs.

Logging

Log the logical document name, counts, duration, and error location. Do not log complete XML, credentials, tokens, or personal data. Bound messages originating from the parser.

Test fragmented characters() events, whitespace, namespaces, copied attributes, excessive depth, giant fields, malformed XML, blocked external entities, unsupported features, early termination, encoding, and persistence rollback.

Common mistakes

Frequent mistakes include assuming one text callback, omitting an element stack, storing attrs without copying, enabling external entities, forgetting limits, using prefixes instead of namespace URIs, continuing after parse errors, leaking state between records, and choosing SAX when backward tree navigation is required.

Conclusion

xml.sax processes XML as a sequence of events and supports large files with controlled memory use. Implement small handlers, accumulate text until closing events, model namespaces by URI, block external entities, impose resource limits, and validate each record before persistence.

Read the official xml.sax documentation and the SAX project reference. For external XML, keep secure defaults and combine streaming with strict resource limits.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    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
    Server rack representing an endpoint built with Python xmlrpc.server
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    xmlrpc.server: Build XML-RPC Servers

    Learn Python xmlrpc.server to build XML-RPC servers, register functions, restrict methods and paths, and avoid unsafe exposure.

    Ler mais

    Tempo de leitura: 5 minutos
    21/08/2026