diff --git a/Doc/library/xml.dom.rst b/Doc/library/xml.dom.rst
index 34e58dcad930125..bbcfc34aa90bfe3 100644
--- a/Doc/library/xml.dom.rst
+++ b/Doc/library/xml.dom.rst
@@ -235,6 +235,11 @@ Node Objects
All of the components of an XML document are subclasses of :class:`Node`.
+.. versionchanged:: next
+ Setting a read-only attribute is deprecated.
+ It now emits a :exc:`DeprecationWarning`
+ and will raise :exc:`NoModificationAllowedErr` in a future version of Python.
+
.. attribute:: Node.nodeType
@@ -304,8 +309,9 @@ All of the components of an XML document are subclasses of :class:`Node`.
.. attribute:: Node.prefix
- The part of the :attr:`tagName` preceding the colon if there is one, else the
- empty string. The value is a string, or ``None``.
+ The part of the :attr:`Element.tagName` preceding the colon if there is one,
+ else the empty string. The value is a string, or ``None``.
+ This is a read-only attribute.
.. attribute:: Node.namespaceURI
@@ -455,12 +461,14 @@ following attributes:
The public identifier for the external subset of the document type definition.
This will be a string or ``None``.
+ This is a read-only attribute.
.. attribute:: DocumentType.systemId
The system identifier for the external subset of the document type definition.
This will be a URI as a string, or ``None``.
+ This is a read-only attribute.
.. attribute:: DocumentType.internalSubset
@@ -474,6 +482,7 @@ following attributes:
The name of the root element as given in the ``DOCTYPE`` declaration, if
present.
+ This is a read-only attribute.
.. attribute:: DocumentType.entities
@@ -587,6 +596,7 @@ of that class.
The element type name. In a namespace-using document it may have colons in it.
The value is a string.
+ This is a read-only attribute.
.. method:: Element.getElementsByTagName(tagName)
@@ -690,6 +700,7 @@ Attr Objects
The attribute name.
In a namespace-using document it may include a colon.
+ This is a read-only attribute.
.. attribute:: Attr.localName
@@ -703,6 +714,7 @@ Attr Objects
The part of the name preceding the colon if there is one, else the
empty string.
+ This is a read-only attribute.
.. attribute:: Attr.value
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index b017535b96979d9..23fb120c3d7a1aa 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -799,6 +799,18 @@ New deprecations
open them one by one instead.
(Contributed by Serhiy Storchaka in :gh:`152638`.)
+* :mod:`xml.dom.minidom`:
+
+ * Setting a read-only attribute of a node is deprecated.
+ It now emits a :exc:`DeprecationWarning`
+ and will raise :exc:`xml.dom.NoModificationAllowedErr` in the future.
+ This affects the :attr:`!nodeType`, :attr:`!nodeName`, :attr:`!name`,
+ :attr:`!tagName`, :attr:`!target`, :attr:`!prefix`, :attr:`!namespaceURI`,
+ :attr:`!publicId` and :attr:`!systemId` attributes.
+ Use ``Document.renameNode()`` to rename an element
+ or an attribute.
+ (Contributed by Serhiy Storchaka in :gh:`57336`.)
+
.. Add deprecations above alphabetically, not here at the end.
.. include:: ../deprecations/pending-removal-in-3.17.rst
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
index 46249e5138aed52..f71b364977b1ba1 100644
--- a/Lib/test/test_minidom.py
+++ b/Lib/test/test_minidom.py
@@ -4,6 +4,7 @@
import pickle
import io
from test import support
+from test.support import warnings_helper
import unittest
import xml.dom.minidom
@@ -777,7 +778,9 @@ def _setupCloneElement(self, deep):
self._testCloneElementCopiesAttributes(
root, clone, "testCloneElement" + (deep and "Deep" or "Shallow"))
# mutilate the original so shared data is detected
- root.tagName = root.nodeName = "MODIFIED"
+ with warnings_helper.check_warnings(
+ ('', DeprecationWarning), quiet=True):
+ root.tagName = root.nodeName = "MODIFIED"
root.setAttribute("attr", "NEW VALUE")
root.setAttribute("added", "VALUE")
return dom, clone
@@ -1325,6 +1328,38 @@ def checkRenameNodeSharedConstraints(self, doc, node):
self.assertRaises(xml.dom.WrongDocumentErr, doc2.renameNode, node,
xml.dom.EMPTY_NAMESPACE, "foo")
+ def test_readonly_attributes(self):
+ # These attributes are read-only in the DOM, and setting them
+ # is deprecated (gh-57336).
+ doc = parseString(''
+ 'text')
+ elem = doc.documentElement
+ attr = elem.attributes["a"]
+ text, comment, pi, cdata = elem.childNodes
+ for node, name in [
+ (doc, "nodeType"), (doc, "nodeName"),
+ (doc.doctype, "name"), (doc.doctype, "nodeName"),
+ (doc.doctype, "publicId"), (doc.doctype, "systemId"),
+ (elem, "nodeType"),
+ (elem, "tagName"), (elem, "nodeName"),
+ (elem, "prefix"), (elem, "namespaceURI"),
+ (attr, "name"), (attr, "nodeName"),
+ (attr, "prefix"), (attr, "namespaceURI"),
+ (text, "nodeName"), (comment, "nodeName"), (cdata, "nodeName"),
+ (pi, "nodeName"), (pi, "target"),
+ ]:
+ with self.subTest(node=type(node).__name__, name=name):
+ value = getattr(node, name)
+ with self.assertWarns(DeprecationWarning):
+ setattr(node, name, value)
+ self.assertEqual(getattr(node, name), value)
+
+ # These are writable.
+ attr.value = "other"
+ text.data = "other"
+ self.assertEqual(attr.value, "other")
+ self.assertEqual(text.data, "other")
+
def testRenameAttribute(self):
doc = parseString("")
elem = doc.documentElement
diff --git a/Lib/xml/dom/minicompat.py b/Lib/xml/dom/minicompat.py
index 5d6fae9a2575bf5..47fe54d7fabdff6 100644
--- a/Lib/xml/dom/minicompat.py
+++ b/Lib/xml/dom/minicompat.py
@@ -40,7 +40,8 @@
# defproperty() should be used for each version of
# the relevant _get_() function.
-__all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty"]
+__all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty",
+ "defdeprecatedproperty"]
import xml.dom
@@ -107,3 +108,22 @@ def set(self, value, name=name):
"expected not to find _set_" + name
prop = property(get, set, doc=doc)
setattr(klass, name, prop)
+
+
+def defdeprecatedproperty(klass, name, doc, private=None):
+ """Define a read-only attribute whose setter is deprecated.
+
+ The value is stored in the *private* attribute ("_" + name by default).
+ Setting the attribute still works, but emits a DeprecationWarning.
+ """
+ if private is None:
+ private = "_" + name
+ def get(self, private=private):
+ return getattr(self, private)
+ def set(self, value, name=name, private=private):
+ import warnings
+ warnings.warn(f"attempt to modify read-only attribute {name!r} "
+ f"is deprecated", DeprecationWarning, stacklevel=2)
+ setattr(self, private, value)
+ prop = property(get, set, doc=doc)
+ setattr(klass, name, prop)
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index 16b33b90184dc59..1b07fc1dd4459e2 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -32,13 +32,19 @@
class Node(xml.dom.Node):
- namespaceURI = None # this is non-null only for elements and attributes
+ _namespaceURI = None # this is non-null only for elements and attributes
parentNode = None
ownerDocument = None
nextSibling = None
previousSibling = None
- prefix = EMPTY_PREFIX # non-null only for NS elements and attributes
+ _prefix = EMPTY_PREFIX # non-null only for NS elements and attributes
+
+ def _get_namespaceURI(self):
+ return self._namespaceURI
+
+ def _get_prefix(self):
+ return self._prefix
def __bool__(self):
return True
@@ -277,6 +283,22 @@ def __enter__(self):
def __exit__(self, et, ev, tb):
self.unlink()
+def _node_get_nodeType(self):
+ return self._nodeType
+Node._get_nodeType = _node_get_nodeType
+del _node_get_nodeType
+def _node_get_nodeName(self):
+ return self._nodeName
+Node._get_nodeName = _node_get_nodeName
+del _node_get_nodeName
+
+defdeprecatedproperty(Node, "nodeType", doc="The type of this node.")
+defdeprecatedproperty(Node, "nodeName", doc="The name of this node.")
+defdeprecatedproperty(Node, "namespaceURI",
+ doc="The namespace URI of this node, or None.")
+defdeprecatedproperty(Node, "prefix",
+ doc="The namespace prefix of this node, or None.")
+
defproperty(Node, "firstChild", doc="First child node, or None.")
defproperty(Node, "lastChild", doc="Last child node, or None.")
defproperty(Node, "localName", doc="Namespace-local name of this node.")
@@ -334,8 +356,8 @@ def _get_elements_by_tagName_ns_helper(parent, nsURI, localName, rc):
return rc
class DocumentFragment(Node):
- nodeType = Node.DOCUMENT_FRAGMENT_NODE
- nodeName = "#document-fragment"
+ _nodeType = Node.DOCUMENT_FRAGMENT_NODE
+ _nodeName = "#document-fragment"
nodeValue = None
attributes = None
parentNode = None
@@ -352,9 +374,9 @@ def __init__(self):
class Attr(Node):
- __slots__=('_name', '_value', 'namespaceURI',
+ __slots__=('_name', '_value', '_namespaceURI',
'_prefix', 'childNodes', '_localName', 'ownerDocument', 'ownerElement')
- nodeType = Node.ATTRIBUTE_NODE
+ _nodeType = Node.ATTRIBUTE_NODE
attributes = None
specified = False
_is_id = False
@@ -366,7 +388,7 @@ def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None,
self.ownerElement = None
self.ownerDocument = None
self._name = qName
- self.namespaceURI = namespaceURI
+ self._namespaceURI = namespaceURI
self._prefix = prefix
if localName is not None:
self._localName = localName
@@ -389,11 +411,17 @@ def _get_specified(self):
def _get_name(self):
return self._name
- def _set_name(self, value):
+ def _rename(self, value):
self._name = value
if self.ownerElement is not None:
_clear_id_cache(self.ownerElement)
+ def _set_name(self, value):
+ import warnings
+ warnings.warn("attempt to modify read-only attribute 'name' "
+ "is deprecated", DeprecationWarning, stacklevel=2)
+ self._rename(value)
+
nodeName = name = property(_get_name, _set_name)
def _get_value(self):
@@ -411,7 +439,7 @@ def _set_value(self, value):
def _get_prefix(self):
return self._prefix
- def _set_prefix(self, prefix):
+ def _reprefix(self, prefix):
nsuri = self.namespaceURI
if prefix == "xmlns":
if nsuri and nsuri != XMLNS_NAMESPACE:
@@ -424,10 +452,19 @@ def _set_prefix(self, prefix):
newName = "%s:%s" % (prefix, self.localName)
if self.ownerElement:
_clear_id_cache(self.ownerElement)
- self.name = newName
+ self._rename(newName)
+
+ def _set_prefix(self, prefix):
+ import warnings
+ warnings.warn("attempt to modify read-only attribute 'prefix' "
+ "is deprecated", DeprecationWarning, stacklevel=2)
+ self._reprefix(prefix)
prefix = property(_get_prefix, _set_prefix)
+ def _get_namespaceURI(self):
+ return self._namespaceURI
+
def unlink(self):
# This implementation does not call the base implementation
# since most of that is not needed, and the expense of the
@@ -478,6 +515,8 @@ def _get_schemaType(self):
defproperty(Attr, "isId", doc="True if this attribute is an ID.")
defproperty(Attr, "localName", doc="Namespace-local name of this attribute.")
defproperty(Attr, "schemaType", doc="Schema type for this attribute.")
+defdeprecatedproperty(Attr, "namespaceURI",
+ doc="Namespace URI of this attribute, or None.")
class NamedNodeMap(object):
@@ -672,10 +711,10 @@ def _get_namespace(self):
_no_type = TypeInfo(None, None)
class Element(Node):
- __slots__=('ownerDocument', 'parentNode', 'tagName', 'nodeName', 'prefix',
- 'namespaceURI', '_localName', 'childNodes', '_attrs', '_attrsNS',
+ __slots__=('ownerDocument', 'parentNode', '_tagName', '_nodeName', '_prefix',
+ '_namespaceURI', '_localName', 'childNodes', '_attrs', '_attrsNS',
'nextSibling', 'previousSibling')
- nodeType = Node.ELEMENT_NODE
+ _nodeType = Node.ELEMENT_NODE
nodeValue = None
schemaType = _no_type
@@ -692,9 +731,9 @@ def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None,
localName=None):
self.ownerDocument = None
self.parentNode = None
- self.tagName = self.nodeName = tagName
- self.prefix = prefix
- self.namespaceURI = namespaceURI
+ self._tagName = self._nodeName = tagName
+ self._prefix = prefix
+ self._namespaceURI = namespaceURI
self.childNodes = NodeList()
self.nextSibling = self.previousSibling = None
@@ -781,8 +820,8 @@ def setAttributeNS(self, namespaceURI, qualifiedName, value):
if attr.isId:
_clear_id_cache(self)
if attr.prefix != prefix:
- attr.prefix = prefix
- attr.nodeName = qualifiedName
+ attr._prefix = prefix
+ attr._rename(qualifiedName)
def getAttributeNode(self, attrname):
if self._attrs is None:
@@ -942,6 +981,20 @@ def setIdAttributeNode(self, idAttr):
self.ownerDocument._magic_id_count += 1
_clear_id_cache(self)
+def _element_get_tagName(self):
+ return self._tagName
+Element._get_tagName = _element_get_tagName
+del _element_get_tagName
+
+defdeprecatedproperty(Element, "tagName",
+ doc="Element name.")
+defdeprecatedproperty(Element, "nodeName",
+ doc="Element name.")
+defdeprecatedproperty(Element, "prefix",
+ doc="Namespace prefix of this element, or None.")
+defdeprecatedproperty(Element, "namespaceURI",
+ doc="Namespace URI of this element, or None.")
+
defproperty(Element, "attributes",
doc="NamedNodeMap of attributes on the element.")
defproperty(Element, "localName",
@@ -1001,13 +1054,16 @@ def replaceChild(self, newChild, oldChild):
class ProcessingInstruction(Childless, Node):
- nodeType = Node.PROCESSING_INSTRUCTION_NODE
- __slots__ = ('target', 'data')
+ _nodeType = Node.PROCESSING_INSTRUCTION_NODE
+ __slots__ = ('_target', 'data')
def __init__(self, target, data):
- self.target = target
+ self._target = target
self.data = data
+ def _get_target(self):
+ return self._target
+
# nodeValue is an alias for data
def _get_nodeValue(self):
return self.data
@@ -1017,15 +1073,24 @@ def _set_nodeValue(self, value):
# nodeName is an alias for target
def _get_nodeName(self):
- return self.target
+ return self._target
+
def _set_nodeName(self, value):
- self.target = value
+ import warnings
+ warnings.warn("attempt to modify read-only attribute 'nodeName' "
+ "is deprecated", DeprecationWarning, stacklevel=2)
+ self._target = value
+
nodeName = property(_get_nodeName, _set_nodeName)
def writexml(self, writer, indent="", addindent="", newl=""):
writer.write("%s%s %s?>%s" % (indent,self.target, self.data, newl))
+defdeprecatedproperty(ProcessingInstruction, "target",
+ doc="The target of this processing instruction.")
+
+
class CharacterData(Childless, Node):
__slots__=('_data', 'ownerDocument','parentNode', 'previousSibling', 'nextSibling')
@@ -1103,8 +1168,8 @@ def replaceData(self, offset, count, arg):
class Text(CharacterData):
__slots__ = ()
- nodeType = Node.TEXT_NODE
- nodeName = "#text"
+ _nodeType = Node.TEXT_NODE
+ _nodeName = "#text"
attributes = None
def splitText(self, offset):
@@ -1210,8 +1275,8 @@ def _get_containing_entref(node):
class Comment(CharacterData):
- nodeType = Node.COMMENT_NODE
- nodeName = "#comment"
+ _nodeType = Node.COMMENT_NODE
+ _nodeName = "#comment"
def __init__(self, data):
CharacterData.__init__(self)
@@ -1226,8 +1291,8 @@ def writexml(self, writer, indent="", addindent="", newl=""):
class CDATASection(Text):
__slots__ = ()
- nodeType = Node.CDATA_SECTION_NODE
- nodeName = "#cdata-section"
+ _nodeType = Node.CDATA_SECTION_NODE
+ _nodeName = "#cdata-section"
def writexml(self, writer, indent="", addindent="", newl=""):
if self.data.find("]]>") >= 0:
@@ -1304,24 +1369,24 @@ def __setstate__(self, state):
class Identified:
"""Mix-in class that supports the publicId and systemId attributes."""
- __slots__ = 'publicId', 'systemId'
+ __slots__ = '_publicId', '_systemId'
def _identified_mixin_init(self, publicId, systemId):
- self.publicId = publicId
- self.systemId = systemId
+ self._publicId = publicId
+ self._systemId = systemId
def _get_publicId(self):
- return self.publicId
+ return self._publicId
def _get_systemId(self):
- return self.systemId
+ return self._systemId
class DocumentType(Identified, Childless, Node):
- nodeType = Node.DOCUMENT_TYPE_NODE
+ _nodeType = Node.DOCUMENT_TYPE_NODE
nodeValue = None
- name = None
- publicId = None
- systemId = None
+ _name = None
+ _publicId = None
+ _systemId = None
internalSubset = None
def __init__(self, qualifiedName):
@@ -1329,8 +1394,14 @@ def __init__(self, qualifiedName):
self.notations = ReadOnlySequentialNamedNodeMap()
if qualifiedName:
prefix, localname = _nssplit(qualifiedName)
- self.name = localname
- self.nodeName = self.name
+ self._name = localname
+ self._nodeName = self._name
+
+ def _get_name(self):
+ return self._name
+
+ def _get_nodeName(self):
+ return self._nodeName
def _get_internalSubset(self):
return self.internalSubset
@@ -1339,8 +1410,8 @@ def cloneNode(self, deep):
if self.ownerDocument is None:
# it's ok
clone = DocumentType(None)
- clone.name = self.name
- clone.nodeName = self.name
+ clone._name = self._name
+ clone._nodeName = self._name
operation = xml.dom.UserDataHandler.NODE_CLONED
if deep:
clone.entities._seq = []
@@ -1378,7 +1449,7 @@ def writexml(self, writer, indent="", addindent="", newl=""):
class Entity(Identified, Node):
attributes = None
- nodeType = Node.ENTITY_NODE
+ _nodeType = Node.ENTITY_NODE
nodeValue = None
actualEncoding = None
@@ -1386,7 +1457,7 @@ class Entity(Identified, Node):
version = None
def __init__(self, name, publicId, systemId, notation):
- self.nodeName = name
+ self._nodeName = name
self.notationName = notation
self.childNodes = NodeList()
self._identified_mixin_init(publicId, systemId)
@@ -1417,14 +1488,26 @@ def replaceChild(self, newChild, oldChild):
"cannot replace children of an entity node")
class Notation(Identified, Childless, Node):
- nodeType = Node.NOTATION_NODE
+ _nodeType = Node.NOTATION_NODE
nodeValue = None
def __init__(self, name, publicId, systemId):
- self.nodeName = name
+ self._nodeName = name
self._identified_mixin_init(publicId, systemId)
+defdeprecatedproperty(DocumentType, "name",
+ doc="The name of the root element as given in the "
+ "DOCTYPE declaration.")
+defdeprecatedproperty(DocumentType, "nodeName",
+ doc="The name of the root element as given in the "
+ "DOCTYPE declaration.")
+defdeprecatedproperty(Identified, "publicId",
+ doc="Public identifier, or None.")
+defdeprecatedproperty(Identified, "systemId",
+ doc="System identifier, or None.")
+
+
class DOMImplementation(DOMImplementationLS):
_features = [("core", "1.0"),
("core", "2.0"),
@@ -1488,8 +1571,8 @@ def createDocument(self, namespaceURI, qualifiedName, doctype):
def createDocumentType(self, qualifiedName, publicId, systemId):
doctype = DocumentType(qualifiedName)
- doctype.publicId = publicId
- doctype.systemId = systemId
+ doctype._publicId = publicId
+ doctype._systemId = systemId
return doctype
# DOM Level 3 (WD 9 April 2002)
@@ -1561,8 +1644,8 @@ class Document(Node, DocumentLS):
Node.COMMENT_NODE, Node.DOCUMENT_TYPE_NODE)
implementation = DOMImplementation()
- nodeType = Node.DOCUMENT_NODE
- nodeName = "#document"
+ _nodeType = Node.DOCUMENT_NODE
+ _nodeName = "#document"
nodeValue = None
attributes = None
parentNode = None
@@ -1873,15 +1956,14 @@ def renameNode(self, n, namespaceURI, name):
element.removeAttributeNode(n)
else:
element = None
- n.prefix = prefix
+ n._prefix = prefix
n._localName = localName
- n.namespaceURI = namespaceURI
- n.nodeName = name
+ n._namespaceURI = namespaceURI
if n.nodeType == Node.ELEMENT_NODE:
- n.tagName = name
+ n._tagName = n._nodeName = name
else:
# attribute node
- n.name = name
+ n._rename(name)
if element is not None:
element.setAttributeNode(n)
if is_id:
diff --git a/Misc/NEWS.d/next/Library/2026-08-12-16-40-00.gh-issue-57336.Xk2mPv.rst b/Misc/NEWS.d/next/Library/2026-08-12-16-40-00.gh-issue-57336.Xk2mPv.rst
new file mode 100644
index 000000000000000..bd3ba397d443173
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-12-16-40-00.gh-issue-57336.Xk2mPv.rst
@@ -0,0 +1,7 @@
+Setting read-only attributes of :mod:`xml.dom.minidom` nodes is now deprecated.
+It emits a :exc:`DeprecationWarning`
+and will raise :exc:`xml.dom.NoModificationAllowedErr` in the future.
+This affects the :attr:`!nodeType`, :attr:`!nodeName`, :attr:`!name`,
+:attr:`!tagName`, :attr:`!target`, :attr:`!prefix`, :attr:`!namespaceURI`,
+:attr:`!publicId` and :attr:`!systemId` attributes.
+Use ``Document.renameNode()`` to rename an element or an attribute.