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

    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