diff --git a/Doc/library/json.rst b/Doc/library/json.rst
index 5e8c452a5ab9a1..ddd12a002f7416 100644
--- a/Doc/library/json.rst
+++ b/Doc/library/json.rst
@@ -261,6 +261,8 @@ Basic Usage
into JSON and then back into a dictionary, the dictionary may not equal
the original one. That is, ``loads(dumps(x)) != x`` if x has non-string
keys.
+ *sort_keys* sorts the keys before they are coerced to strings,
+ so numeric keys are sorted by value, not by their string representation.
.. function:: load(fp, *, cls=None, object_hook=None, parse_float=None, \
parse_int=None, parse_constant=None, \
diff --git a/Doc/library/tk.rst b/Doc/library/tk.rst
index e27af48ba7ac48..9ca26a5dfa7659 100644
--- a/Doc/library/tk.rst
+++ b/Doc/library/tk.rst
@@ -34,7 +34,7 @@ alternative `GUI frameworks and tools ', ('html', None, None)),
+ (']>', ('html', None, None)),
+ ('', ('html', None, 'a.dtd')),
+ (']>',
+ ('html', None, 'a.dtd')),
+ ('', ('html', '-//P', 'a.dtd')),
+ ("",
+ ('html', '-//P', 'a.dtd')),
+ ]:
+ with self.subTest(doctype=doctype):
+ parser = ET.XMLParser(target=DoctypeParser())
+ parser.feed(doctype + '')
+ self.assertEqual(parser.close(), expected)
+
def test_builder_lookup_errors(self):
class RaisingBuilder:
def __init__(self, raise_in=None, what=ValueError):
diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py
index 5b663a4e456456..1349e49fbd68a5 100644
--- a/Lib/tkinter/font.py
+++ b/Lib/tkinter/font.py
@@ -104,7 +104,8 @@ def __init__(self, root=None, font=None, name=None, exists=False,
if exists:
self.name = name
# confirm font exists
- if self.name not in tk.splitlist(tk.call("font", "names")):
+ name = getattr(name, 'string', name) # can be a Tcl object
+ if name not in tk.splitlist(tk.call("font", "names")):
raise tkinter._tkinter.TclError(
"named font %s does not already exist" % (self.name,))
# if font config info supplied, apply it
@@ -123,11 +124,11 @@ def __init__(self, root=None, font=None, name=None, exists=False,
self._call = tk.call
def __str__(self):
- # A wrapped description is a list or tuple, not a string; format it as
- # a Tcl word so it can be used as an option value (as ttk does).
- if isinstance(self.name, str):
- return self.name
- return tkinter._join(self.name)
+ # A wrapped description can be a list or tuple; format it as a Tcl
+ # word so it can be used as an option value (as ttk does).
+ if isinstance(self.name, (list, tuple)):
+ return tkinter._join(self.name)
+ return str(self.name)
def __repr__(self):
return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \
@@ -136,7 +137,13 @@ def __repr__(self):
def __eq__(self, other):
if not isinstance(other, Font):
return NotImplemented
- return self.name == other.name and self._tk == other._tk
+ name = self.name
+ other_name = other.name
+ if type(name) is not type(other_name):
+ # A Tcl object does not compare equal to a string.
+ name = getattr(name, 'string', name)
+ other_name = getattr(other_name, 'string', other_name)
+ return name == other_name and self._tk == other._tk
def __getitem__(self, key):
return self.cget(key)
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index bed8c27df5a384..ce98e4dc24a0d3 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -1591,10 +1591,10 @@ def __init__(self, *, target=None, encoding=None):
parser.CommentHandler = target.comment
if hasattr(target, 'pi'):
parser.ProcessingInstructionHandler = target.pi
+ parser.StartDoctypeDeclHandler = self._start_doctype
# Configure pyexpat: buffering, new-style attribute handling.
parser.buffer_text = 1
parser.ordered_attributes = 1
- self._doctype = None
self.entity = {}
try:
self.version = "Expat %d.%d.%d" % expat.version_info
@@ -1713,38 +1713,15 @@ def _default(self, text):
err.lineno = self.parser.ErrorLineNumber
err.offset = self.parser.ErrorColumnNumber
raise err
- elif prefix == "<" and text[:9] == "":
- self._doctype = None
- return
- text = text.strip(_XML_WHITESPACE)
- if not text:
- return
- self._doctype.append(text)
- n = len(self._doctype)
- if n > 2:
- type = self._doctype[1]
- if type == "PUBLIC" and n == 4:
- name, type, pubid, system = self._doctype
- if pubid:
- pubid = pubid[1:-1]
- elif type == "SYSTEM" and n == 3:
- name, type, system = self._doctype
- pubid = None
- else:
- return
- if hasattr(self.target, "doctype"):
- self.target.doctype(name, pubid, system[1:-1])
- elif hasattr(self, "doctype"):
- warnings.warn(
- "The doctype() method of XMLParser is ignored. "
- "Define doctype() method on the TreeBuilder target.",
- RuntimeWarning)
-
- self._doctype = None
+
+ def _start_doctype(self, name, system, pubid, has_internal_subset):
+ if hasattr(self.target, "doctype"):
+ self.target.doctype(name, pubid, system)
+ elif hasattr(self, "doctype"):
+ warnings.warn(
+ "The doctype() method of XMLParser is ignored. "
+ "Define doctype() method on the TreeBuilder target.",
+ RuntimeWarning)
def feed(self, data):
"""Feed encoded data to parser."""
diff --git a/Misc/NEWS.d/next/Documentation/2026-06-18-16-58-52.gh-issue-59551.oNkjTG.rst b/Misc/NEWS.d/next/Documentation/2026-06-18-16-58-52.gh-issue-59551.oNkjTG.rst
new file mode 100644
index 00000000000000..9842e85f9c2c21
--- /dev/null
+++ b/Misc/NEWS.d/next/Documentation/2026-06-18-16-58-52.gh-issue-59551.oNkjTG.rst
@@ -0,0 +1 @@
+Rename Doc/library/dialog.rst to tkinter.dialogs.rst.
diff --git a/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst b/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst
new file mode 100644
index 00000000000000..9df2868c30f16d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-05-22-39-22.gh-issue-156961.ZtYqoA.rst
@@ -0,0 +1,3 @@
+Fix :func:`tkinter.font.nametofont` and the :class:`tkinter.font.Font`
+constructor for a font name or description returned by Tk as a Tcl object,
+for example by :meth:`ttk.Style.lookup() `.
diff --git a/Misc/NEWS.d/next/Library/2026-09-11-12-00-00.gh-issue-156955.bLtmAp.rst b/Misc/NEWS.d/next/Library/2026-09-11-12-00-00.gh-issue-156955.bLtmAp.rst
new file mode 100644
index 00000000000000..2a452a2a6e1652
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-11-12-00-00.gh-issue-156955.bLtmAp.rst
@@ -0,0 +1,3 @@
+Speed up :func:`csv.writer` by caching the set of characters that need
+quoting or escaping in the dialect. Writing long fields is now up to 5 times
+faster.
diff --git a/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst b/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst
new file mode 100644
index 00000000000000..53f2bf561eefbb
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst
@@ -0,0 +1,4 @@
+Fix the Python implementation of :class:`xml.etree.ElementTree.XMLParser`:
+the ``doctype()`` method of the target is now called for a document type
+declaration without an external identifier, like ````,
+as in the C implementation.
diff --git a/Modules/_csv.c b/Modules/_csv.c
index c640f2d36a8464..6af66c3f09a03b 100644
--- a/Modules/_csv.c
+++ b/Modules/_csv.c
@@ -117,7 +117,12 @@ typedef struct {
Py_UCS4 quotechar; /* quote character */
Py_UCS4 escapechar; /* escape character */
PyObject *lineterminator; /* string to write between records */
-
+ /* Cache for the writer: bit c is set if the ASCII character c needs
+ quoting or escaping (delimiter, quotechar, escapechar, '\r', '\n'
+ and the characters of lineterminator). */
+ uint64_t special_chars[2];
+ /* Whether any of the special characters is non-ASCII. */
+ bool nonascii_special;
} DialectObj;
typedef struct {
@@ -332,6 +337,54 @@ _set_str(const char *name, PyObject **target, PyObject *src, const char *dflt)
return 0;
}
+static void
+dialect_add_special_char(DialectObj *self, Py_UCS4 c)
+{
+ if (c == NOT_SET) {
+ return;
+ }
+ if (c < 128) {
+ self->special_chars[c / 64] |= (uint64_t)1 << (c % 64);
+ }
+ else {
+ self->nonascii_special = true;
+ }
+}
+
+static void
+dialect_init_special_chars_cache(DialectObj *self)
+{
+ self->special_chars[0] = self->special_chars[1] = 0;
+ self->nonascii_special = false;
+ dialect_add_special_char(self, self->delimiter);
+ dialect_add_special_char(self, self->quotechar);
+ dialect_add_special_char(self, self->escapechar);
+ dialect_add_special_char(self, '\r');
+ dialect_add_special_char(self, '\n');
+ PyObject *lt = self->lineterminator;
+ for (Py_ssize_t i = 0; i < PyUnicode_GET_LENGTH(lt); i++) {
+ dialect_add_special_char(self, PyUnicode_READ_CHAR(lt, i));
+ }
+}
+
+/* Whether the character needs quoting or escaping by the writer. */
+static inline bool
+dialect_is_special_char(DialectObj *self, Py_UCS4 c)
+{
+ if (c < 128) {
+ return (self->special_chars[c / 64] >> (c % 64)) & 1;
+ }
+ if (!self->nonascii_special) {
+ return false;
+ }
+ return (c == self->delimiter ||
+ c == self->quotechar ||
+ c == self->escapechar ||
+ PyUnicode_FindChar(self->lineterminator, c, 0,
+ PyUnicode_GET_LENGTH(self->lineterminator),
+ 1) >= 0);
+}
+
static int
dialect_check_quoting(int quoting)
{
@@ -558,6 +611,7 @@ dialect_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
goto err;
}
+ dialect_init_special_chars_cache(self);
ret = Py_NewRef(self);
err:
@@ -1208,14 +1262,7 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data,
Py_UCS4 c = PyUnicode_READ(field_kind, field_data, i);
int want_escape = 0;
- if (c == dialect->delimiter ||
- c == dialect->escapechar ||
- c == dialect->quotechar ||
- c == '\n' ||
- c == '\r' ||
- PyUnicode_FindChar(
- dialect->lineterminator, c, 0,
- PyUnicode_GET_LENGTH(dialect->lineterminator), 1) >= 0) {
+ if (dialect_is_special_char(dialect, c)) {
if (dialect->quoting == QUOTE_NONE)
want_escape = 1;
else {