The xml.parsers.expat module exposes the Expat XML parser directly to Python. It is fast, event-driven, and non-validating. Instead of building a tree automatically, an application registers callbacks for element starts, text, comments, namespaces, declarations, and other XML events.
This level of control is useful for protocol tools, converters, analyzers, and integrations that need performance or specialized callbacks. For most application work, Python ElementTree or Python xml.sax provides a simpler interface.
Create a parser
Use ParserCreate(). An optional encoding overrides the document declaration. Expat natively supports a limited set that includes UTF-8, UTF-16, ISO-8859-1, and ASCII.
from xml.parsers import expat
parser = expat.ParserCreate()
A parser instance can process only one XML document. Create a new parser for each document.
Register handlers
Handlers are assigned directly to parser attributes.
from xml.parsers import expat
def start(name, attributes):
print("start", name, attributes)
def end(name):
print("end", name)
def text(data):
if data.strip():
print("text", repr(data))
parser = expat.ParserCreate()
parser.StartElementHandler = start
parser.EndElementHandler = end
parser.CharacterDataHandler = text
parser.Parse("<root><item id='1'>Hello</item></root>", True)
The final argument to Parse() must be true on the last call. It tells Expat no more input will arrive and allows checks for incomplete tokens.
Incremental parsing
A document can be supplied in chunks. This helps with large files and streams, but total volume still needs an explicit limit.
MAX_BYTES = 20 * 1024 * 1024
received = 0
with open("data.xml", "rb") as file:
while chunk := file.read(64 * 1024):
received += len(chunk)
if received > MAX_BYTES:
raise ValueError("XML is too large")
parser.Parse(chunk, False)
parser.Parse(b"", True)
Also limit events, depth, accumulated text, and runtime. Chunking avoids one large allocation but does not prevent expansion attacks.
Character data may be fragmented
Expat may call CharacterDataHandler several times for one logical text value, especially around newlines or chunk boundaries. Accumulate fragments until the corresponding element ends.
parts = []
parser.buffer_text = True
def text(data):
parts.append(data)
buffer_text=True reduces callback count but does not guarantee one callback per element. buffer_size controls the text buffer size.
Namespaces
Pass a one-character separator to ParserCreate() to enable namespace processing.
parser = expat.ParserCreate(namespace_separator=" ")
def start(name, attributes):
namespace, _, local = name.partition(" ")
print(namespace, local)
Names become the namespace URI, separator, and local part. Do not depend on the source prefix.
Positions and errors
During callbacks, CurrentLineNumber, CurrentColumnNumber, and CurrentByteIndex report the current event position. After an error, use the corresponding error attributes and ErrorCode.
from xml.parsers import expat
try:
parser.Parse("<root><item></root>", True)
except expat.ExpatError as error:
message = expat.ErrorString(error.code)
print(message, error.lineno, error.offset)
Do not log the entire XML document in production. Record the origin, position, and a small redacted context only when safe.
Ordered and specified attributes
Attributes are dictionaries by default. With ordered_attributes=True, they arrive as a list alternating names and values in source order. XML attribute order should not carry semantic meaning, so use this mainly for diagnostics or faithful reproduction.
specified_attributes=True reports only attributes present in the document and omits defaults derived from declarations. This requires careful DTD knowledge.
External entities
ExternalEntityRefHandler can load external resources, but it may expose local files and network access. For user-controlled XML, do not implement a handler that opens the supplied systemId.
The guide to Python xmlreader shows how to block external sources in SAX readers. The same principle applies here: the document must not select arbitrary resources.
Parameter entities and DTDs
SetParamEntityParsing() controls parameter entities, and UseForeignDTD() can request an alternate DTD. These features increase attack surface and should remain disabled for untrusted documents.
Reparse deferral
Expat 2.6 introduced reparse deferral to avoid quadratic work when very large unfinished tokens arrive in small chunks. Calling SetReparseDeferralEnabled(False) disables that protection and may reintroduce denial-of-service risk.
if hasattr(parser, "GetReparseDeferralEnabled"):
assert parser.GetReparseDeferralEnabled()
Keep it enabled. A handler may not fire immediately after each chunk; that delay is part of the protection.
Billion laughs protection
Python 3.14.6 may expose methods to tune entity-amplification protection.
if hasattr(parser, "SetBillionLaughsAttackProtectionActivationThreshold"):
parser.SetBillionLaughsAttackProtectionActivationThreshold(8 * 1024 * 1024)
if hasattr(parser, "SetBillionLaughsAttackProtectionMaximumAmplification"):
parser.SetBillionLaughsAttackProtectionMaximumAmplification(100.0)
Defaults depend on the linked Expat library. Values that are too low may reject legitimate documents. Test real payloads and never raise limits without understanding the threat model.
Memory amplification protection
Recent versions also expose allocation tracking controls.
if hasattr(parser, "SetAllocTrackerActivationThreshold"):
parser.SetAllocTrackerActivationThreshold(64 * 1024 * 1024)
if hasattr(parser, "SetAllocTrackerMaximumAmplification"):
parser.SetAllocTrackerMaximumAmplification(100.0)
These controls complement but do not replace application limits for bytes, time, depth, and event count.
Comments, CDATA, and instructions
Handlers exist for comments, CDATA boundaries, processing instructions, XML declarations, DTD declarations, and namespaces. CharacterDataHandler receives both regular text and CDATA contents; CDATA handlers identify the syntactic boundaries.
GetInputContext()
During a callback, GetInputContext() may return source data around the current event. Use it only for controlled diagnostics because it can expose sensitive content.
ParseFile()
ParseFile() accepts an object that implements read(nbytes). It is convenient but provides less direct control over byte accounting and cancellation than an explicit loop with Parse().
Encoding errors
Unsupported encodings and invalid byte sequences raise ExpatError. Do not override the encoding merely to suppress errors. Correct the source or perform a deliberate conversion step.
Recommended tests
Test complete and fragmented input, split character data, namespaces, encodings, truncated documents, mismatched tags, duplicate attributes, huge tokens, internal entities, DTDs, blocked external entities, excessive nesting, and amplification controls.
Common mistakes
Common failures include reusing a parser for several documents, forgetting the final flag, assuming one text callback, disabling reparse deferral, loading external entities without policy, relying only on internal protections, and omitting application-level limits.
Choosing another API
Use Expat directly for low-level callbacks, performance, and fine control. Use ElementTree for trees, SAX for a standardized event interface, and Python pulldom for selective DOM fragments.
Conclusion
xml.parsers.expat is fast and detailed, but the application must manage state, fragmented text, limits, and security. Keep reparse deferral and amplification protections enabled, avoid external entities, and create one parser per document.
Consult the official Python Expat documentation and the official Expat project.







