Python saxutils: XML Utility Functions

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

The xml.sax.saxutils module collects helper functions and classes commonly used by SAX-based XML applications. It can escape text, quote attribute values, decode known entities, generate XML from SAX events, place filters between a parser and an application, and normalize parser input sources.

Although its name is tied to SAX, several functions are useful outside an event-driven parser. They must still be applied in the correct context. XML escaping is not HTML sanitization, SQL parameterization, shell quoting, path validation, or general input security.

Main tools in saxutils

The best-known functions are escape(), unescape(), and quoteattr(). The main classes are XMLGenerator and XMLFilterBase. The module also provides prepare_input_source() to normalize values accepted by XML parsers.

Escape element text

escape() replaces &, <, and > with XML entities. This lets arbitrary text appear inside an element without changing document structure.

from xml.sax.saxutils import escape

text = "5 < 10 & 12 > 8"
safe = escape(text)
print(safe)
# 5 &lt; 10 &amp; 12 &gt; 8

These three characters are always escaped. The optional entities mapping adds replacements, but the function should not become a general string translation engine.

result = escape(
    "line 1\nline 2",
    {"\n": "
"},
)

Add custom entities only when the target format requires them. Arbitrary replacements may create unreadable or incompatible XML.

Escaping depends on context

Element text and attribute values are different contexts. escape() does not surround an attribute with quotes or choose how to handle single and double quote characters. Use quoteattr() for attributes.

Do not use escape() as a policy for complete user-supplied HTML. It protects a text-node insertion, but it does not analyze tags, URLs, scripts, CSS, or dangerous attributes. The guide to Python html.entities explains the difference between entities, decoding, and sanitization.

Quote attributes with quoteattr()

quoteattr() escapes required characters, chooses a quote delimiter, and returns the value already surrounded by quotes.

from xml.sax.saxutils import quoteattr

value = 'Final "report" & approved'
attribute = quoteattr(value)
print(f"<file name={attribute}/>")

If the value contains only one quote type, the function tries to use the other delimiter. When it contains both, required quotes are encoded.

Avoid assembling large XML documents through string concatenation. For structured output, use ElementTree, minidom, or XMLGenerator. See Python ElementTree for a tree-oriented API.

Decode entities with unescape()

unescape() converts &amp;, &lt;, and &gt; to their literal characters.

from xml.sax.saxutils import unescape

text = "Tom &amp; Ana &lt;3 XML"
print(unescape(text))

An optional mapping can define additional entities. However, unescape() is not a complete XML parser. It does not validate structure, namespaces, encodings, DTDs, or malformed documents.

Avoid decoding the same data twice. Double decoding may convert text that was intentionally literal into active markup. Define one clear layer where entity decoding occurs.

Generate XML with XMLGenerator

XMLGenerator implements the SAX ContentHandler interface and writes events back as XML. It can reproduce parsed events or generate a new document.

from io import StringIO
from xml.sax.saxutils import XMLGenerator
from xml.sax.xmlreader import AttributesImpl

output = StringIO()
generator = XMLGenerator(
    output,
    encoding="utf-8",
    short_empty_elements=True,
)

generator.startDocument()
generator.startElement("catalog", AttributesImpl({"version": "1"}))
generator.startElement("product", AttributesImpl({"id": "42"}))
generator.characters("Coffee & code")
generator.endElement("product")
generator.endElement("catalog")
generator.endDocument()

print(output.getvalue())

The generator escapes text and attributes according to the events it receives. Keep events properly nested: start the document, open elements, write character data, close elements in reverse order, and finish the document.

Output encoding

The encoding configured on XMLGenerator must match the output stream strategy. With StringIO, the result is Unicode text. For files and binary streams, verify how bytes are produced and test non-ASCII characters.

The guide to Python codecs covers encodings, BOM handling, incremental decoding, and error policies.

Transform events with XMLFilterBase

XMLFilterBase sits between an XMLReader and the final handler. By default it passes configuration requests and events unchanged. A subclass can intercept events to rename elements, remove attributes, normalize text, or block selected data.

from xml.sax.saxutils import XMLFilterBase
from xml.sax.xmlreader import AttributesImpl

class RemoveSecret(XMLFilterBase):
    def startElement(self, name, attrs):
        if "secret" in attrs:
            updated = dict(attrs.items())
            updated.pop("secret", None)
            attrs = AttributesImpl(updated)
        super().startElement(name, attrs)

A filter must preserve a valid event sequence. If it removes a start event, it must also handle the corresponding end event. Test namespaces, fragmented text, comments, empty elements, and processing instructions.

The guide to Python xml.sax explains handlers, namespaces, locators, and callback flow.

Normalize sources with prepare_input_source()

prepare_input_source() converts a string, file-like object, or existing InputSource into an input source ready for a parser. A base URL may help resolve relative identifiers.

from xml.sax.saxutils import prepare_input_source

source = prepare_input_source("data.xml")
print(source.getSystemId())

Automatic path or URL resolution can be dangerous when a source is controlled by users. Restrict protocols, hosts, allowed directories, redirects, and byte size. Do not turn an arbitrary identifier into an unrestricted network request.

Security for external XML

The module does not remove parser vulnerabilities. Keep external entities disabled, limit bytes and nesting depth, and prevent documents from freely selecting local files or remote resources. The guide to Python pulldom shows practical limits for large XML streams.

When generating XML, validate element and attribute names. escape() protects text content but does not make an arbitrary string a valid XML name. Prefer names defined by trusted application code or a schema.

Avoid XML built by concatenation

A common mistake is to join raw strings:

# Avoid this for real documents
xml = "<user name=\"" + name + "\">" + text + "</user>"

Even with escaping, it is easy to miss a context, produce invalid encoding declarations, or unbalance tags. For tiny fragments, quoteattr() and escape() may be enough. For complete documents, prefer a structural API.

Test ampersands, angle brackets, both quote types, Unicode, empty values, line breaks, already escaped text, and characters forbidden by XML. For generators, test empty elements, namespaces, attributes, and text or byte streams.

For filters, parse the generated output in tests instead of comparing only a string. A visually plausible document may still be malformed.

Common mistakes

Common failures include using escape() for attributes, calling unescape() twice, treating escaping as sanitization, concatenating tags with external data, leaving encoding implicit, enabling external entities, and altering event streams without preserving matching start and end events.

Conclusion

xml.sax.saxutils provides practical building blocks for XML text escaping, attribute quoting, event-based generation, SAX filtering, and input-source normalization.

Apply each tool in the correct context, prefer structural APIs for full documents, and enforce security limits for external data. Consult the official saxutils documentation and the W3C XML specification.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    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
    A solitary tree with sunlight filtering through the leaves on a grassy hill under a blue sky.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ElementTree: Read and Modify XML in Python

    Learn Python ElementTree to read, search, modify, and generate XML with namespaces, incremental parsing, limits, and security.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python poplib: Read Email with POP3

    Learn Python poplib to access POP3 with TLS, list and download messages, use UIDL, enforce limits, and avoid accidental deletion.

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python imaplib: Read Email with IMAP

    Learn Python imaplib to access IMAP mailboxes with TLS, search by UID, read without marking messages, manage flags, and avoid

    Ler mais

    Tempo de leitura: 5 minutos
    22/08/2026