https://github.com/python/cpython/commit/532b9dbd678b6d8e51d4199db8bd7d6b157fdea4
commit: 532b9dbd678b6d8e51d4199db8bd7d6b157fdea4
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-01T18:01:47+03:00
summary:

gh-89499: Support the standalone document declaration in ElementTree (GH-156730)

ElementTree.write(), tostring() and tostringlist() get the standalone
parameter: True for standalone="yes", False for standalone="no", and None
(the default) to omit it.  An XML declaration is written if it is not None,
and combining it with a false xml_declaration is an error.

files:
A Misc/NEWS.d/next/Library/2026-08-31-19-30-00.gh-issue-89499.Kx8vQ1.rst
M Doc/library/xml.etree.elementtree.rst
M Lib/test/test_xml_etree.py
M Lib/xml/etree/ElementTree.py

diff --git a/Doc/library/xml.etree.elementtree.rst 
b/Doc/library/xml.etree.elementtree.rst
index 310ccd651e18c7e..f9ffb07ada88fd3 100644
--- a/Doc/library/xml.etree.elementtree.rst
+++ b/Doc/library/xml.etree.elementtree.rst
@@ -711,16 +711,16 @@ Functions
 
 .. function:: tostring(element, encoding="us-ascii", method="xml", *, \
                        xml_declaration=None, default_namespace=None, \
-                       short_empty_elements=True)
+                       short_empty_elements=True, standalone=None)
 
    Generates a string representation of an XML element, including all
    subelements.  *element* is an :class:`Element` instance.  *encoding* [1]_ is
    the output encoding (default is US-ASCII).  Use ``encoding="unicode"`` to
    generate a Unicode string (otherwise, a bytestring is generated).  *method*
    is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
-   *xml_declaration*, *default_namespace* and *short_empty_elements* has the 
same
-   meaning as in :meth:`ElementTree.write`. Returns an (optionally) encoded 
string
-   containing the XML data.
+   *xml_declaration*, *default_namespace*, *short_empty_elements* and
+   *standalone* has the same meaning as in :meth:`ElementTree.write`.
+   Returns an (optionally) encoded string containing the XML data.
 
    .. versionchanged:: 3.4
       Added the *short_empty_elements* parameter.
@@ -732,19 +732,23 @@ Functions
       The :func:`tostring` function now preserves the attribute order
       specified by the user.
 
+   .. versionchanged:: next
+      Added the *standalone* parameter.
+
 
 .. function:: tostringlist(element, encoding="us-ascii", method="xml", *, \
                            xml_declaration=None, default_namespace=None, \
-                           short_empty_elements=True)
+                           short_empty_elements=True, standalone=None)
 
    Generates a string representation of an XML element, including all
    subelements.  *element* is an :class:`Element` instance.  *encoding* [1]_ is
    the output encoding (default is US-ASCII).  Use ``encoding="unicode"`` to
    generate a Unicode string (otherwise, a bytestring is generated).  *method*
    is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
-   *xml_declaration*, *default_namespace* and *short_empty_elements* has the 
same
-   meaning as in :meth:`ElementTree.write`. Returns a list of (optionally) 
encoded
-   strings containing the XML data. It does not guarantee any specific 
sequence,
+   *xml_declaration*, *default_namespace*, *short_empty_elements* and
+   *standalone* has the same meaning as in :meth:`ElementTree.write`.
+   Returns a list of (optionally) encoded strings containing the XML data.
+   It does not guarantee any specific sequence,
    except that ``b"".join(tostringlist(element)) == tostring(element)``.
 
    .. versionadded:: 3.2
@@ -759,6 +763,9 @@ Functions
       The :func:`tostringlist` function now preserves the attribute order
       specified by the user.
 
+   .. versionchanged:: next
+      Added the *standalone* parameter.
+
 
 .. function:: XML(text, parser=None)
 
@@ -1186,7 +1193,7 @@ ElementTree Objects
 
    .. method:: write(file, encoding="us-ascii", xml_declaration=None, \
                      default_namespace=None, method="xml", *, \
-                     short_empty_elements=True)
+                     short_empty_elements=True, standalone=None)
 
       Writes the element tree to a file, as XML.  *file* is a file name, or a
       :term:`file object` opened for writing.  *encoding* [1]_ is the output
@@ -1202,6 +1209,13 @@ ElementTree Objects
       emitted as a single self-closed tag, otherwise they are emitted as a pair
       of start/end tags.
 
+      The keyword-only *standalone* parameter is the value of the standalone
+      document declaration in the XML declaration.
+      Use ``True`` for ``standalone="yes"``, ``False`` for ``standalone="no"``,
+      and ``None`` (the default) to omit it.
+      An XML declaration is written if *standalone* is not ``None``;
+      combining it with ``xml_declaration=False`` raises a :exc:`ValueError`.
+
       The output is either a string (:class:`str`) or binary (:class:`bytes`).
       This is controlled by the *encoding* argument.  If *encoding* is
       ``"unicode"``, the output is a string; otherwise, it's binary.  Note that
@@ -1216,6 +1230,9 @@ ElementTree Objects
          The :meth:`write` method now preserves the attribute order specified
          by the user.
 
+      .. versionchanged:: next
+         Added the *standalone* parameter.
+
 
 This is the XML file that is going to be manipulated::
 
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index fae212e7d2d369f..a0337de58b23ae4 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -970,6 +970,88 @@ def test_tostring_xml_declaration_cases(self):
                     expected_retval
                 )
 
+    def test_tostring_standalone(self):
+        elem = ET.XML('<body><tag/></body>')
+        self.assertEqual(
+            ET.tostring(elem, encoding='unicode', standalone=True),
+            "<?xml version='1.0' encoding='utf-8' standalone='yes'?>\n"
+            "<body><tag /></body>"
+        )
+        self.assertEqual(
+            ET.tostring(elem, encoding='unicode', standalone=False),
+            "<?xml version='1.0' encoding='utf-8' standalone='no'?>\n"
+            "<body><tag /></body>"
+        )
+        # the XML declaration is written even if it would be omitted
+        self.assertEqual(
+            ET.tostring(elem, standalone=True),
+            b"<?xml version='1.0' encoding='us-ascii' standalone='yes'?>\n"
+            b"<body><tag /></body>"
+        )
+        self.assertEqual(
+            ET.tostring(elem, encoding='UTF-8', standalone=False),
+            b"<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n"
+            b"<body><tag /></body>"
+        )
+
+    def test_tostring_standalone_none(self):
+        elem = ET.XML('<body><tag/></body>')
+        self.assertEqual(
+            ET.tostring(elem, encoding='unicode', standalone=None),
+            '<body><tag /></body>'
+        )
+        self.assertEqual(
+            ET.tostring(elem, encoding='unicode', xml_declaration=True,
+                        standalone=None),
+            "<?xml version='1.0' encoding='utf-8'?>\n<body><tag /></body>"
+        )
+
+    def test_tostring_standalone_without_xml_declaration(self):
+        elem = ET.XML('<body><tag/></body>')
+        for standalone in True, False:
+            for xml_declaration in False, 0, '':
+                with self.subTest(standalone=standalone,
+                                  xml_declaration=xml_declaration):
+                    with self.assertRaises(ValueError):
+                        ET.tostring(elem, xml_declaration=xml_declaration,
+                                    standalone=standalone)
+
+    def test_tostring_standalone_text_method(self):
+        elem = ET.XML('<body><tag>text</tag></body>')
+        self.assertEqual(
+            ET.tostring(elem, encoding='unicode', method='text',
+                        standalone=True),
+            'text'
+        )
+
+    def test_tostringlist_standalone(self):
+        elem = ET.XML('<body><tag/></body>')
+        self.assertEqual(
+            b''.join(ET.tostringlist(elem, standalone=True)),
+            b"<?xml version='1.0' encoding='us-ascii' standalone='yes'?>\n"
+            b"<body><tag /></body>"
+        )
+        with self.assertRaises(ValueError):
+            ET.tostringlist(elem, xml_declaration=False, standalone=False)
+
+    def test_write_standalone(self):
+        elem = ET.XML('<body><tag/></body>')
+        tree = ET.ElementTree(elem)
+        for standalone, expected in [
+                (True, "standalone='yes'"), (False, "standalone='no'")]:
+            with self.subTest(standalone=standalone):
+                file = io.StringIO()
+                tree.write(file, encoding='unicode', standalone=standalone)
+                self.assertEqual(
+                    file.getvalue(),
+                    "<?xml version='1.0' encoding='utf-8' %s?>\n"
+                    "<body><tag /></body>" % expected
+                )
+        file = io.StringIO()
+        with self.assertRaises(ValueError):
+            tree.write(file, encoding='unicode', xml_declaration=False,
+                       standalone=True)
+
     def test_tostringlist_default_namespace(self):
         elem = ET.XML('<body xmlns="http://effbot.org/ns";><tag/></body>')
         self.assertEqual(
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index 951540eb9f45e90..1cba3eae7ad629d 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -697,7 +697,8 @@ def write(self, file_or_filename,
               xml_declaration=None,
               default_namespace=None,
               method=None, *,
-              short_empty_elements=True):
+              short_empty_elements=True,
+              standalone=None):
         """Write element tree to a file as XML.
 
         Arguments:
@@ -722,6 +723,10 @@ def write(self, file_or_filename,
                                     self-closed tag, otherwise they are
                                     emitted as a pair of start/end tags
 
+          *standalone* -- bool for the standalone document declaration in
+                          the XML declaration.  If None (default), the
+                          standalone document declaration is omitted
+
         """
         if self._root is None:
             raise TypeError('ElementTree not initialized')
@@ -729,6 +734,11 @@ def write(self, file_or_filename,
             method = "xml"
         elif method not in _serialize:
             raise ValueError("unknown method %r" % method)
+        if standalone is not None:
+            if xml_declaration is not None and not xml_declaration:
+                raise ValueError("the standalone document declaration "
+                                 "requires the XML declaration")
+            xml_declaration = True
         if not encoding:
             encoding = "us-ascii"
         with _get_writer(file_or_filename, encoding) as (write, 
declared_encoding):
@@ -738,8 +748,13 @@ def write(self, file_or_filename,
                     (xml_declaration is None and
                      encoding.lower() != "unicode" and
                      declared_encoding.lower() not in ("utf-8", "us-ascii"))):
-                write("<?xml version='1.0' encoding='%s'?>\n" % (
-                    declared_encoding,))
+                if standalone is None:
+                    sddecl = ""
+                else:
+                    sddecl = " standalone='%s'" % (
+                        "yes" if standalone else "no",)
+                write("<?xml version='1.0' encoding='%s'%s?>\n" % (
+                    declared_encoding, sddecl))
             if method == "text":
                 _serialize_text(write, self._root)
             else:
@@ -1085,7 +1100,7 @@ def _escape_attrib_html(text):
 
 def tostring(element, encoding=None, method=None, *,
              xml_declaration=None, default_namespace=None,
-             short_empty_elements=True):
+             short_empty_elements=True, standalone=None):
     """Generate string representation of XML element.
 
     All subelements are included.  If encoding is "unicode", a string
@@ -1094,7 +1109,9 @@ def tostring(element, encoding=None, method=None, *,
     *element* is an Element instance, *encoding* is an optional output
     encoding defaulting to US-ASCII, *method* is an optional output which
     can be one of "xml" (default), "html" or "text",
-    *default_namespace* sets the default XML namespace (for "xmlns").
+    *default_namespace* sets the default XML namespace (for "xmlns"),
+    *standalone* is the value of the standalone document declaration
+    in the XML declaration (omitted if None).
 
     Returns an (optionally) encoded string containing the XML data.
 
@@ -1104,7 +1121,8 @@ def tostring(element, encoding=None, method=None, *,
                                xml_declaration=xml_declaration,
                                default_namespace=default_namespace,
                                method=method,
-                               short_empty_elements=short_empty_elements)
+                               short_empty_elements=short_empty_elements,
+                               standalone=standalone)
     return stream.getvalue()
 
 class _ListDataStream(io.BufferedIOBase):
@@ -1126,14 +1144,15 @@ def tell(self):
 
 def tostringlist(element, encoding=None, method=None, *,
                  xml_declaration=None, default_namespace=None,
-                 short_empty_elements=True):
+                 short_empty_elements=True, standalone=None):
     lst = []
     stream = _ListDataStream(lst)
     ElementTree(element).write(stream, encoding,
                                xml_declaration=xml_declaration,
                                default_namespace=default_namespace,
                                method=method,
-                               short_empty_elements=short_empty_elements)
+                               short_empty_elements=short_empty_elements,
+                               standalone=standalone)
     return lst
 
 
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-31-19-30-00.gh-issue-89499.Kx8vQ1.rst 
b/Misc/NEWS.d/next/Library/2026-08-31-19-30-00.gh-issue-89499.Kx8vQ1.rst
new file mode 100644
index 000000000000000..d0b8ead7506af0a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-31-19-30-00.gh-issue-89499.Kx8vQ1.rst
@@ -0,0 +1,5 @@
+:meth:`ElementTree.write() <xml.etree.ElementTree.ElementTree.write>`,
+:func:`~xml.etree.ElementTree.tostring` and
+:func:`~xml.etree.ElementTree.tostringlist` now support the *standalone*
+parameter, the value of the standalone document declaration
+in the XML declaration.

_______________________________________________
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