Python expat: Low-Level XML Parsing

Published on: August 23, 2026
Reading time: 4 minutes
A developer typing code on a laptop with a Python book beside in an office.

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.

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ElementInclude: Safe XInclude

    Learn Python ElementInclude to use XInclude with safe loaders, base URLs, maximum depth, and blocked external paths.

    Ler mais

    Tempo de leitura: 4 minutos
    23/08/2026
    Close-up of a saxophone and sheet music stand, perfect for jazz lovers and musicians.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xmlreader: Control SAX Parsers

    Learn Python xmlreader to configure SAX parsers, InputSource, incremental parsing, attributes, locators, and safer XML handling.

    Ler mais

    Tempo de leitura: 4 minutos
    23/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python saxutils: XML Utility Functions

    Learn Python saxutils to escape XML, quote attributes, generate documents, build SAX filters, and avoid context mistakes.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pulldom: Partial DOM for XML

    Learn Python pulldom to process XML events, expand only selected subtrees, and reduce memory use with safer limits.

    Ler mais

    Tempo de leitura: 5 minutos
    23/08/2026
    A laptop screen showing a code editor with visible programming code in a dimly lit environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python xml.sax: Process XML Events

    Learn Python xml.sax to process XML as events with low memory use, namespaces, handlers, limits, and secure entity handling.

    Ler mais

    Tempo de leitura: 4 minutos
    22/08/2026
    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