diff --git a/Doc/library/xml.dom.minidom.rst b/Doc/library/xml.dom.minidom.rst
index efc81f31e36a5b..fd307b062515f3 100644
--- a/Doc/library/xml.dom.minidom.rst
+++ b/Doc/library/xml.dom.minidom.rst
@@ -154,6 +154,10 @@ module documentation. This section lists the differences between the API and
.. versionchanged:: 3.9
The *standalone* parameter was added.
+ .. versionchanged:: next
+ Namespace declarations missing for the serialized element
+ and its attributes are now written.
+
.. method:: Node.toxml(encoding=None, standalone=None)
Return a string or byte string containing the XML represented by
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
index 3735a6046891ea..888376a39c84db 100644
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -561,6 +561,92 @@ def testWriteXML(self):
dom.unlink()
self.assertEqual(str, domstr)
+ def testWriteXMLNamespaceDeclarations(self):
+ dom = Document()
+ root = dom.appendChild(
+ dom.createElementNS("http://xml.python.org/ns", "p:root"))
+ child = root.appendChild(
+ dom.createElementNS("http://xml.python.org/ns", "p:child"))
+ child.setAttributeNS("http://xml.python.org/ns2", "q:attr", "value")
+ self.assertEqual(dom.documentElement.toxml(),
+ ''
+ '')
+ dom.unlink()
+
+ def testWriteXMLDefaultNamespace(self):
+ dom = Document()
+ root = dom.appendChild(
+ dom.createElementNS("http://xml.python.org/ns", "root"))
+ root.appendChild(
+ dom.createElementNS("http://xml.python.org/ns", "child"))
+ # An element in no namespace undeclares the default namespace.
+ root.appendChild(dom.createElement("nons"))
+ self.assertEqual(dom.documentElement.toxml(),
+ ''
+ '')
+ dom.unlink()
+
+ def testWriteXMLAttributeNamespacePrefix(self):
+ dom = Document()
+ root = dom.appendChild(dom.createElement("root"))
+ # Attributes cannot use the default namespace, a prefix is invented.
+ root.setAttributeNS("http://xml.python.org/ns", "attr", "value")
+ root.setAttributeNS("http://xml.python.org/ns2", "attr2", "value2")
+ self.assertEqual(dom.documentElement.toxml(),
+ '')
+ dom.unlink()
+
+ def testWriteXMLXMLPrefix(self):
+ dom = Document()
+ root = dom.appendChild(dom.createElement("root"))
+ # The "xml" prefix is bound by definition and is never declared.
+ root.setAttributeNS(xml.dom.XML_NAMESPACE, "xml:lang", "en")
+ self.assertEqual(dom.documentElement.toxml(), '')
+ dom.unlink()
+
+ def testWriteXMLExistingNamespaceDeclarations(self):
+ for str in [
+ '',
+ '',
+ ''
+ '',
+ '',
+ ]:
+ with self.subTest(str=str):
+ dom = parseString(str)
+ self.assertEqual(dom.documentElement.toxml(), str)
+ dom.unlink()
+
+ def testWriteXMLNotANamespaceDeclaration(self):
+ # an attribute whose name only starts with "xmlns" is not one
+ dom = parseString(''
+ '')
+ self.assertEqual(dom.documentElement.toxml(),
+ ''
+ '')
+ dom.unlink()
+
+ dom = Document()
+ root = dom.appendChild(
+ dom.createElementNS("http://xml.python.org/ns", "root"))
+ child = root.appendChild(dom.createElement("child"))
+ child.setAttribute("xmlnsabc", "v")
+ self.assertEqual(dom.documentElement.toxml(),
+ ''
+ '')
+ dom.unlink()
+
+ def testWriteXMLDoesNotModifyDocument(self):
+ dom = Document()
+ root = dom.appendChild(
+ dom.createElementNS("http://xml.python.org/ns", "p:root"))
+ root.toxml()
+ self.assertEqual(root.attributes.length, 0)
+ dom.unlink()
+
def test_toxml_quote_text(self):
dom = Document()
elem = dom.appendChild(dom.createElement('elem'))
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index 5fd3911bd3c9eb..e4adbd94555c07 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -19,7 +19,8 @@
import xml
import xml.dom
-from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg
+from xml.dom import (EMPTY_NAMESPACE, EMPTY_PREFIX, XML_NAMESPACE,
+ XMLNS_NAMESPACE, domreg)
from xml.dom.minicompat import *
from xml.dom.xmlbuilder import DOMImplementationLS, DocumentLS
@@ -346,6 +347,100 @@ def _write_data(writer, text, attr):
text = text.replace("\t", " ")
writer.write(text)
+
+# The "xml" prefix is bound by definition and is never declared.
+_ROOT_NSMAP = {"xml": XML_NAMESPACE}
+
+
+def _bind_namespace(nsmap, inherited, prefix, uri):
+ """Bind *prefix* in *nsmap*, copying it if it is still the inherited one."""
+ if nsmap is inherited:
+ nsmap = dict(inherited)
+ nsmap[prefix] = uri
+ return nsmap
+
+
+def _in_scope_namespaces(element):
+ """Return the namespaces in scope for *element*, as written by writexml."""
+ ancestors = []
+ node = element.parentNode
+ while node is not None and node.nodeType == Node.ELEMENT_NODE:
+ ancestors.append(node)
+ node = node.parentNode
+ nsmap = _ROOT_NSMAP
+ for node in reversed(ancestors):
+ nsmap, _ = _fixup_namespaces(node, nsmap)
+ return nsmap
+
+
+def _fixup_namespaces(element, nsmap):
+ """Compute namespace declarations missing for the serialized element.
+
+ *nsmap* is the mapping of prefixes to namespace URIs in scope for the
+ element. Return the mapping in scope for its children and the list of
+ (name, value) pairs of the attributes to be written, starting with the
+ added namespace declarations. The element and its attributes are not
+ modified.
+ """
+ attrs = element._attrs
+ uri = element.namespaceURI
+ if not attrs and not uri and not nsmap.get(None):
+ # Neither the element nor its attributes need a declaration.
+ return nsmap, ()
+
+ inherited = nsmap
+ declarations = []
+ # (name, value, namespace URI, attribute) of the attributes to write.
+ entries = []
+ if attrs:
+ for attr in attrs.values():
+ name = attr.name
+ attr_uri = attr.namespaceURI
+ if (attr_uri == XMLNS_NAMESPACE or name == "xmlns"
+ or name.startswith("xmlns:")):
+ # Declarations already present in the document take precedence.
+ nsmap = _bind_namespace(
+ nsmap, inherited,
+ attr.localName if attr.prefix else None, attr.value)
+ attr_uri = None
+ elif attr_uri == XML_NAMESPACE:
+ # The xml prefix is bound by definition.
+ attr_uri = None
+ entries.append((name, attr.value, attr_uri, attr))
+
+ if uri:
+ prefix, _, _ = element.tagName.rpartition(':')
+ prefix = prefix or None
+ if nsmap.get(prefix) != uri:
+ nsmap = _bind_namespace(nsmap, inherited, prefix, uri)
+ declarations.append(("xmlns:" + prefix if prefix else "xmlns", uri))
+ elif nsmap.get(None) and ':' not in element.tagName:
+ # The element is in no namespace, undeclare the default one.
+ nsmap = _bind_namespace(nsmap, inherited, None, None)
+ declarations.append(("xmlns", ""))
+
+ items = []
+ for name, value, attr_uri, attr in entries:
+ if attr_uri is not None:
+ # Unprefixed attributes are in no namespace, so an attribute
+ # in a namespace always needs a prefix.
+ prefix, _, _ = name.rpartition(':')
+ if not prefix:
+ n = 0
+ while nsmap.get("ns%d" % n) is not None:
+ n += 1
+ prefix = "ns%d" % n
+ name = "%s:%s" % (prefix, attr.localName)
+ if nsmap.get(prefix) != attr_uri:
+ nsmap = _bind_namespace(nsmap, inherited, prefix, attr_uri)
+ declarations.append(("xmlns:" + prefix, attr_uri))
+ items.append((name, value))
+
+ if declarations:
+ return nsmap, declarations + items
+ return nsmap, items
+
+
def _get_elements_by_tagName_helper(parent, name, rc):
for node in parent.childNodes:
if node.nodeType == Node.ELEMENT_NODE and \
@@ -914,7 +1009,8 @@ def getElementsByTagNameNS(self, namespaceURI, localName):
def __repr__(self):
return "" % (self.tagName, id(self))
- def writexml(self, writer, indent="", addindent="", newl=""):
+ def writexml(self, writer, indent="", addindent="", newl="", *,
+ _nsmap=None):
"""Write an XML element to a file-like object
Write the element to the writer object that must provide
@@ -923,13 +1019,14 @@ def writexml(self, writer, indent="", addindent="", newl=""):
# indent = current indentation
# addindent = indentation to add to higher levels
# newl = newline string
+ if _nsmap is None:
+ _nsmap = _in_scope_namespaces(self)
writer.write(indent+"<" + self.tagName)
- attrs = self._get_attributes()
-
- for a_name in attrs.keys():
+ nsmap, items = _fixup_namespaces(self, _nsmap)
+ for a_name, value in items:
writer.write(" %s=\"" % a_name)
- _write_data(writer, attrs[a_name].value, True)
+ _write_data(writer, value, True)
writer.write("\"")
if self.childNodes:
writer.write(">")
@@ -940,7 +1037,15 @@ def writexml(self, writer, indent="", addindent="", newl=""):
else:
writer.write(newl)
for node in self.childNodes:
- node.writexml(writer, indent+addindent, addindent, newl)
+ if type(node).writexml is Element.writexml:
+ # Pass the namespaces in scope to the standard
+ # implementation; an overridden writexml() has the
+ # documented signature and computes them itself.
+ node.writexml(writer, indent+addindent, addindent,
+ newl, _nsmap=nsmap)
+ else:
+ node.writexml(writer, indent+addindent, addindent,
+ newl)
writer.write(indent)
writer.write("%s>%s" % (self.tagName, newl))
else:
diff --git a/Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-44376.Kp4Vz9.rst b/Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-44376.Kp4Vz9.rst
new file mode 100644
index 00000000000000..5fce84ec0c52a8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-44376.Kp4Vz9.rst
@@ -0,0 +1,4 @@
+:meth:`~xml.dom.minidom.Node.writexml` in :mod:`xml.dom.minidom` now writes
+the namespace declarations needed to serialize the namespaces of the element
+and its attributes, if they are not already declared for an ancestor. The
+document is not modified.