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.
Recommended tests
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.







