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

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026