https://github.com/python/cpython/commit/e7c93b7e5c2cb3ffe076845edd6b5c287af3bc4c
commit: e7c93b7e5c2cb3ffe076845edd6b5c287af3bc4c
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-02T15:30:21+03:00
summary:

gh-156658: Only XML white space characters are treated as white space 
(GH-156659)

XML defines white space as " \t\r\n" (see XML 1.0, 2.3), but str.strip()
also strips other characters, such as U+00A0.  Such characters could be
lost in ElementTree.indent(), in canonicalize(strip_text=True), and when
parsing with the whitespace-in-element-content feature turned off.

files:
A Misc/NEWS.d/next/Library/2026-08-30-19-30-00.gh-issue-156658.Xq5Nt7.rst
M Lib/test/test_minidom.py
M Lib/test/test_xml_etree.py
M Lib/xml/dom/expatbuilder.py
M Lib/xml/dom/minidom.py
M Lib/xml/etree/ElementTree.py

diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
index 3735a6046891ea9..e204bdc7dc672db 100644
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -642,6 +642,30 @@ def test_toprettyxml_preserves_content_of_text_node(self):
                 dom.getElementsByTagName('B')[0].childNodes[0].toxml(),
                 dom2.getElementsByTagName('B')[0].childNodes[0].toxml())
 
+    def test_isWhitespaceInElementContent(self):
+        # only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
+        dom = parseString('<!DOCTYPE a [<!ELEMENT a (b)*><!ELEMENT b 
(#PCDATA)>]>'
+                          '<a> <b>x</b>\xa0</a>')
+        children = dom.documentElement.childNodes
+        self.assertTrue(children[0].isWhitespaceInElementContent)
+        self.assertFalse(children[2].isWhitespaceInElementContent)
+        dom.unlink()
+
+    def test_remove_whitespace_in_element_content(self):
+        from xml.dom.xmlbuilder import DOMBuilder, DOMInputSource
+        builder = DOMBuilder()
+        builder.setFeature("whitespace-in-element-content", False)
+        source = DOMInputSource()
+        source.byteStream = io.BytesIO(
+            b'<!DOCTYPE a [<!ELEMENT a (b)*><!ELEMENT b (#PCDATA)>]>'
+            b'<a> <b>x</b>\xc2\xa0</a>')
+        dom = builder.parse(source)
+        children = dom.documentElement.childNodes
+        # ignorable whitespace is removed, other characters are not
+        self.assertEqual([node.nodeName for node in children], ['b', '#text'])
+        self.assertEqual(children[1].data, '\xa0')
+        dom.unlink()
+
     def testProcessingInstruction(self):
         dom = parseString('<e><?mypi \t\n data \t\n ?></e>')
         pi = dom.documentElement.firstChild
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index a0337de58b23ae4..f9ff8c4c3541eda 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -845,6 +845,15 @@ def test_indent_space_caching(self):
             len({id(el.tail) for el in elem.iter()}),
         )
 
+    def test_indent_non_xml_whitespace(self):
+        # only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
+        elem = ET.XML('<html>\xa0<body><p>text</p>\xa0</body></html>')
+        ET.indent(elem)
+        self.assertEqual(
+            ET.tostring(elem),
+            b'<html>&#160;<body>\n    <p>text</p>&#160;</body>\n</html>'
+        )
+
     def test_indent_level(self):
         elem = 
ET.XML("<html><body><p>pre<br/>post</p><p>text</p></body></html>")
         with self.assertRaises(ValueError):
@@ -4900,6 +4909,11 @@ def test_simple_roundtrip(self):
         xml = '<X xmlns="http://nps/a";><Y xmlns:b="http://nsp/b"; 
b:targets="abc,xyz"></Y></X>'
         self.assertEqual(c14n_roundtrip(xml), xml)
 
+    def test_c14n_strip_non_xml_whitespace(self):
+        # only " \t\r\n" are whitespace in XML (see XML 1.0, 2.3)
+        self.assertEqual(c14n_roundtrip("<a> \xa0x\xa0 </a>", strip_text=True),
+                         "<a>\xa0x\xa0</a>")
+
     def test_c14n_exclusion(self):
         xml = textwrap.dedent("""\
         <root xmlns:x="http://example.com/x";>
diff --git a/Lib/xml/dom/expatbuilder.py b/Lib/xml/dom/expatbuilder.py
index d56b2ddfdb25698..e3917c1cc682880 100644
--- a/Lib/xml/dom/expatbuilder.py
+++ b/Lib/xml/dom/expatbuilder.py
@@ -30,7 +30,8 @@
 from xml.dom import xmlbuilder, minidom, Node
 from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE
 from xml.parsers import expat
-from xml.dom.minidom import _append_child, _set_attribute_node
+from xml.dom.minidom import (_append_child, _set_attribute_node,
+                            _XML_WHITESPACE)
 from xml.dom.NodeFilter import NodeFilter
 
 TEXT_NODE = Node.TEXT_NODE
@@ -413,7 +414,8 @@ def _handle_white_text_nodes(self, node, info):
         # whitespace.
         L = []
         for child in node.childNodes:
-            if child.nodeType == TEXT_NODE and not child.data.strip():
+            if (child.nodeType == TEXT_NODE
+                    and not child.data.strip(_XML_WHITESPACE)):
                 L.append(child)
 
         # Remove ignorable whitespace from the tree.
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index 5fd3911bd3c9ebb..7639fa14c5050fb 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -31,6 +31,9 @@
 _nodeTypes_with_children = (xml.dom.Node.ELEMENT_NODE,
                             xml.dom.Node.ENTITY_REFERENCE_NODE)
 
+# The white space characters of the XML specification (see XML 1.0, 2.3).
+_XML_WHITESPACE = " \t\r\n"
+
 
 class Node(xml.dom.Node):
     namespaceURI = None # this is non-null only for elements and attributes
@@ -1209,7 +1212,7 @@ def replaceWholeText(self, content):
             return None
 
     def _get_isWhitespaceInElementContent(self):
-        if self.data.strip():
+        if self.data.strip(_XML_WHITESPACE):
             return False
         elem = _get_containing_element(self)
         if elem is None:
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index 1cba3eae7ad629d..2fee57df59fdd83 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -101,6 +101,9 @@
 from . import ElementPath
 
 
+# The white space characters of the XML specification (see XML 1.0, 2.3).
+_XML_WHITESPACE = " \t\r\n"
+
 class ParseError(SyntaxError):
     """An error when parsing an XML document.
 
@@ -1209,17 +1212,17 @@ def _indent_children(elem, level):
             child_indentation = indentations[level] + space
             indentations.append(child_indentation)
 
-        if not elem.text or not elem.text.strip():
+        if not elem.text or not elem.text.strip(_XML_WHITESPACE):
             elem.text = child_indentation
 
         for child in elem:
             if len(child):
                 _indent_children(child, child_level)
-            if not child.tail or not child.tail.strip():
+            if not child.tail or not child.tail.strip(_XML_WHITESPACE):
                 child.tail = child_indentation
 
         # Dedent after the last child by overwriting the previous indentation.
-        if not child.tail.strip():
+        if not child.tail.strip(_XML_WHITESPACE):
             child.tail = indentations[level]
 
     _indent_children(tree, 0)
@@ -1724,7 +1727,7 @@ def _default(self, text):
             if prefix == ">":
                 self._doctype = None
                 return
-            text = text.strip()
+            text = text.strip(_XML_WHITESPACE)
             if not text:
                 return
             self._doctype.append(text)
@@ -1940,7 +1943,7 @@ def _flush(self, _join_text=''.join):
         data = _join_text(self._data)
         del self._data[:]
         if self._strip_text and not self._preserve_space[-1]:
-            data = data.strip()
+            data = data.strip(_XML_WHITESPACE)
         if self._pending_start is not None:
             args, self._pending_start = self._pending_start, None
             qname_text = data if data and _looks_like_prefix_name(data) else 
None
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-30-19-30-00.gh-issue-156658.Xq5Nt7.rst 
b/Misc/NEWS.d/next/Library/2026-08-30-19-30-00.gh-issue-156658.Xq5Nt7.rst
new file mode 100644
index 000000000000000..3f2f105116c1838
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-30-19-30-00.gh-issue-156658.Xq5Nt7.rst
@@ -0,0 +1,5 @@
+:mod:`xml.dom` and :mod:`xml.etree.ElementTree` no longer treat characters
+which are not white space in XML (such as U+00A0) as white space.  Previously
+they could be lost in :func:`~xml.etree.ElementTree.indent`,
+:func:`~xml.etree.ElementTree.canonicalize` with ``strip_text=True``, and when
+parsing with the ``whitespace-in-element-content`` feature turned off.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to