diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
index 446bbe096bd19dc..b19a0cfd94b8ddd 100644
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -1605,6 +1605,21 @@ def testNormalizeDeleteAndCombine(self):
, "testNormalizeDeleteAndCombine -- result")
doc.unlink()
+ def testNormalizeManyTextNodes(self):
+ # Ensure that normalize() is fast with many adjacent text nodes.
+ N = 100_000
+ doc = parseString("")
+ root = doc.documentElement
+ for i in range(N):
+ root.appendChild(doc.createTextNode("x" * 16))
+ if i % 3 == 0:
+ root.appendChild(doc.createTextNode(""))
+ doc.normalize()
+ self.assertEqual(len(root.childNodes), 1)
+ self.assertEqual(root.firstChild.data, "x" * 16 * N)
+ self.assertIsNone(root.firstChild.nextSibling)
+ doc.unlink()
+
def testNormalizeRecursion(self):
doc = parseString(""
""
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index 7cb652a323dcc22..d4166a2bce20e5e 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -196,6 +196,9 @@ def removeChild(self, oldChild):
def normalize(self):
L = []
+ # (text node, [data, ...]) for each text node that absorbs others.
+ # Join each run once at the end; concatenating as we go is quadratic.
+ runs = None
for child in self.childNodes:
if child.nodeType == Node.TEXT_NODE:
if not child.data:
@@ -208,7 +211,12 @@ def normalize(self):
elif L and L[-1].nodeType == child.nodeType:
# collapse text node
node = L[-1]
- node.data = node.data + child.data
+ if runs is None:
+ runs = []
+ if runs and runs[-1][0] is node:
+ runs[-1][1].append(child.data)
+ else:
+ runs.append((node, [node.data, child.data]))
node.nextSibling = child.nextSibling
if child.nextSibling:
child.nextSibling.previousSibling = node
@@ -219,6 +227,9 @@ def normalize(self):
L.append(child)
if child.nodeType == Node.ELEMENT_NODE:
child.normalize()
+ if runs is not None:
+ for node, data in runs:
+ node.data = ''.join(data)
self.childNodes[:] = L
def cloneNode(self, deep):
diff --git a/Misc/NEWS.d/next/Library/2026-10-05-16-40-00.gh-issue-158860.Mn7dXk.rst b/Misc/NEWS.d/next/Library/2026-10-05-16-40-00.gh-issue-158860.Mn7dXk.rst
new file mode 100644
index 000000000000000..3e4051e21beee0c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-05-16-40-00.gh-issue-158860.Mn7dXk.rst
@@ -0,0 +1,2 @@
+:meth:`xml.dom.minidom.Node.normalize` now merges runs of adjacent text
+nodes in linear time instead of quadratic time.