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 < 10 & 12 > 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 &, <, and > to their literal characters.
from xml.sax.saxutils import unescape
text = "Tom & Ana <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.
Recommended tests
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.







