The xml.sax.xmlreader module defines the interfaces used by SAX parsers in Python. It describes how a parser receives input sources, sends events to handlers, configures features and properties, reports line and column positions, and supports incremental parsing.
Most applications create a parser with xml.sax.make_parser(), but understanding XMLReader, InputSource, Locator, and attribute objects provides better control over security, encoding, streams, and parser extensions.
The role of XMLReader
XMLReader is the base interface implemented by SAX parser drivers. A driver exposes a create_parser() function, and make_parser() uses it to create a reader. The reader processes XML and invokes registered handlers.
from xml.sax import make_parser
from xml.sax.handler import ContentHandler
class MyHandler(ContentHandler):
def startElement(self, name, attrs):
print("start:", name)
parser = make_parser()
parser.setContentHandler(MyHandler())
parser.parse("data.xml")
When parse() returns, the document has been fully processed. Use the incremental interface when chunks arrive over time or blocking behavior is not acceptable.
Configurable handlers
A reader can receive a ContentHandler, DTDHandler, EntityResolver, and ErrorHandler. Without a content handler, content events are discarded. Without an error handler, errors normally become exceptions and warnings may be printed.
The guide to Python xml.sax explains the main callbacks. Python saxutils provides generators and filters for the same event flow.
Control origins with InputSource
InputSource stores a public identifier, system identifier, encoding, byte stream, and character stream. It lets an application provide a source that is already open instead of allowing the reader to open a path or URL automatically.
from xml.sax import make_parser
from xml.sax.xmlreader import InputSource
source = InputSource()
source.setSystemId("local-import")
source.setEncoding("utf-8")
source.setByteStream(open("data.xml", "rb"))
parser = make_parser()
parser.setContentHandler(MyHandler())
parser.parse(source)
When a character stream exists, the parser ignores the byte stream and the encoding value on the input source. A byte stream takes precedence over opening the system identifier.
Byte streams and character streams
A byte stream allows the parser to inspect the XML encoding declaration unless an encoding is explicitly supplied. A character stream is already decoded, so the application becomes responsible for correct decoding.
Do not decode arbitrary XML bytes with errors="ignore" before parsing. Removed characters may change document meaning. The guide to Python codecs covers encodings and error policies.
Prevent automatic URL access
A string passed to parse() may represent a filename, path-like object, or system identifier. With external data, open the source yourself, enforce size and protocol restrictions, and provide a stream. This prevents a user-controlled reference from becoming an unrestricted network or file access.
Combine this with an EntityResolver that rejects external entities. XML input should not freely choose local files, remote hosts, or protocols.
Parser features
getFeature() and setFeature() read or change boolean SAX options. Common features include namespace processing, namespace prefixes, validation, and external entities. A parser may not recognize or support every feature.
from xml.sax import make_parser
from xml.sax.handler import feature_namespaces, feature_external_ges
parser = make_parser()
parser.setFeature(feature_namespaces, True)
try:
parser.setFeature(feature_external_ges, False)
except Exception as exc:
raise RuntimeError("Required XML protection is unavailable") from exc
Do not silently ignore a failed security configuration. If an essential protection cannot be guaranteed, reject the operation.
Parser properties
Properties carry more complex objects, including lexical handlers. getProperty() and setProperty() may raise SAXNotRecognizedException or SAXNotSupportedException. Configure properties before parsing begins.
Incremental parsing
IncrementalParser accepts chunks through feed(). After the final chunk, close() checks well-formedness conditions that can only be verified at the end and sends remaining events. Call reset() after close() before reusing the parser.
from xml.sax import make_parser
parser = make_parser()
parser.setContentHandler(MyHandler())
with open("large.xml", "rb") as file:
while chunk := file.read(64 * 1024):
parser.feed(chunk)
parser.close()
Do not mix parse() and feed() during the same operation. Do not call reset() while a document is being processed.
Limits still matter
Reading in chunks does not limit total size. Count input bytes, events, depth, accumulated text, and runtime. Stop before policy limits are exceeded.
MAX_BYTES = 20 * 1024 * 1024
received = 0
while chunk := origin.read(64 * 1024):
received += len(chunk)
if received > MAX_BYTES:
raise ValueError("XML exceeds the size limit")
parser.feed(chunk)
parser.close()
Locator for line and column
A Locator associates an event with the current document position. The parser provides it through setDocumentLocator(). Values are valid only during handler callbacks.
from xml.sax.handler import ContentHandler
class PositionedHandler(ContentHandler):
def setDocumentLocator(self, locator):
self.locator = locator
def startElement(self, name, attrs):
line = self.locator.getLineNumber()
column = self.locator.getColumnNumber()
print(name, line, column)
Copy location values during the event. Keeping the locator and consulting it later will report a different position.
AttributesImpl
AttributesImpl represents attributes passed to startElement(). It supports part of the mapping protocol plus methods such as getLength(), getNames(), getType(), and getValue().
A parser may reuse the object. Copy attributes when they must survive the callback.
def startElement(self, name, attrs):
copied = dict(attrs.items())
self.records.append((name, copied))
Namespace-aware attributes
With namespaces enabled, startElementNS() receives element names as (namespaceURI, localname) tuples. AttributesNSImpl works with the same form and can convert between qualified names and namespace pairs.
Do not rely only on textual prefixes. Prefixes may change while the namespace URI remains the same.
Error handling
An error handler receives warnings, recoverable errors, and fatal errors. Data pipelines should usually stop on structural errors and return a controlled diagnostic. Avoid logging the complete XML document or credentials.
A blocking entity resolver
from io import StringIO
from xml.sax.handler import EntityResolver
from xml.sax.xmlreader import InputSource
class BlockExternal(EntityResolver):
def resolveEntity(self, publicId, systemId):
source = InputSource()
source.setCharacterStream(StringIO(""))
return source
parser.setEntityResolver(BlockExternal())
Depending on the application, raising an exception may be safer than returning an empty source. The important rule is to avoid automatic external access.
Recommended tests
Test strings, Path values, byte streams, character streams, declared and overridden encodings, namespaces, copied attributes, incremental input, truncated documents, external entities, unsupported features, byte limits, and locator positions.
Common mistakes
Frequent mistakes include trusting external system IDs, decoding character streams incorrectly, forgetting close(), reusing a parser without reset(), storing attribute objects without copying, enabling external entities, and assuming all parsers support the same features.
Conclusion
xml.sax.xmlreader defines the infrastructure behind SAX parsers: readers, input sources, handlers, features, properties, locators, and attribute interfaces. Understanding it gives applications stronger control over input, encoding, streaming, and security.
Open external sources with limits, keep external entities disabled, and configure the reader before parsing. Consult the official xmlreader documentation and Python’s XML security guidance.







