From e32789e915a4a6e156eac73d6512c01f3587619d Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sat, 22 Aug 2026 20:16:37 +0200 Subject: [PATCH 1/4] gh-129711: Add a no-escape fast path to the _json str escapers ascii_escape_unicode() and escape_unicode() now return the input bulk-copied with surrounding quotes when nothing needs escaping, via a shared quote_unescaped_unicode() helper, like the writer-based write_escaped_ascii()/write_escaped_unicode() already do in-place. This speeds up json.dumps() of clean strings by 1.2x-2.2x. No behavior change. --- ...-08-13-09-00-00.gh-issue-129711.eScFP1.rst | 1 + Modules/_json.c | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst diff --git a/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst b/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst new file mode 100644 index 000000000000000..dfe82854fa24ed1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst @@ -0,0 +1 @@ +Speed up :func:`json.dump` for strings that need no escaping. diff --git a/Modules/_json.c b/Modules/_json.c index 3a724a3e72b185b..aced8007328c825 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -223,6 +223,28 @@ ascii_escape_unicode_and_size(const void *input, int kind, Py_ssize_t input_char return rval; } +static PyObject * +quote_unescaped_unicode(PyObject *pystr) +{ + Py_ssize_t len = PyUnicode_GET_LENGTH(pystr); + PyObject *rval = PyUnicode_New(len + 2, PyUnicode_MAX_CHAR_VALUE(pystr)); + if (rval == NULL) { + return NULL; + } + int kind = PyUnicode_KIND(rval); + void *data = PyUnicode_DATA(rval); + PyUnicode_WRITE(kind, data, 0, '"'); + if (PyUnicode_CopyCharacters(rval, 1, pystr, 0, len) < 0) { + Py_DECREF(rval); + return NULL; + } + PyUnicode_WRITE(kind, data, len + 1, '"'); +#ifdef Py_DEBUG + assert(_PyUnicode_CheckConsistency(rval, 1)); +#endif + return rval; +} + static PyObject * ascii_escape_unicode(PyObject *pystr) { @@ -236,6 +258,10 @@ ascii_escape_unicode(PyObject *pystr) return NULL; } + if (output_size == input_chars + 2) { + return quote_unescaped_unicode(pystr); + } + return ascii_escape_unicode_and_size(input, kind, input_chars, output_size); } @@ -383,6 +409,10 @@ escape_unicode(PyObject *pystr) return NULL; } + if (output_size == input_chars + 2) { + return quote_unescaped_unicode(pystr); + } + return escape_unicode_and_size(input, kind, maxchar, input_chars, output_size); } From 3c3051ac742fa1acf7f5a873a2e22cb59e0b4f37 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 25 Aug 2026 23:52:52 +0200 Subject: [PATCH 2/4] Copy with memcpy in quote_unescaped_unicode and fix NEWS issue number Co-Authored-By: Claude Fable 5 --- ...rst => 2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst} | 0 Modules/_json.c | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename Misc/NEWS.d/next/Library/{2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst => 2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst b/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-129711.eScFP1.rst rename to Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst diff --git a/Modules/_json.c b/Modules/_json.c index aced8007328c825..2fbc75e8eeb8a1f 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -233,11 +233,11 @@ quote_unescaped_unicode(PyObject *pystr) } int kind = PyUnicode_KIND(rval); void *data = PyUnicode_DATA(rval); + /* No escapes means the output has the same maxchar class as the input, + so PyUnicode_New() gave us the same kind and a raw copy is valid. */ + assert(kind == PyUnicode_KIND(pystr)); PyUnicode_WRITE(kind, data, 0, '"'); - if (PyUnicode_CopyCharacters(rval, 1, pystr, 0, len) < 0) { - Py_DECREF(rval); - return NULL; - } + memcpy((char *)data + kind, PyUnicode_DATA(pystr), (size_t)len * kind); PyUnicode_WRITE(kind, data, len + 1, '"'); #ifdef Py_DEBUG assert(_PyUnicode_CheckConsistency(rval, 1)); From d1e7e1053ce9aaf022606f706ddaeba4374f7127 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 30 Aug 2026 20:48:55 +0200 Subject: [PATCH 3/4] gh-156241: Split quote_unescaped_unicode into ascii and unicode variants Co-Authored-By: Claude Fable 5 --- Modules/_json.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Modules/_json.c b/Modules/_json.c index 2fbc75e8eeb8a1f..199c9c5438b95e6 100644 --- a/Modules/_json.c +++ b/Modules/_json.c @@ -223,6 +223,25 @@ ascii_escape_unicode_and_size(const void *input, int kind, Py_ssize_t input_char return rval; } +static PyObject * +quote_unescaped_ascii(PyObject *pystr) +{ + assert(PyUnicode_IS_ASCII(pystr)); + Py_ssize_t len = PyUnicode_GET_LENGTH(pystr); + PyObject *rval = PyUnicode_New(len + 2, 127); + if (rval == NULL) { + return NULL; + } + Py_UCS1 *output = PyUnicode_1BYTE_DATA(rval); + output[0] = '"'; + memcpy(output + 1, PyUnicode_1BYTE_DATA(pystr), (size_t)len); + output[len + 1] = '"'; +#ifdef Py_DEBUG + assert(_PyUnicode_CheckConsistency(rval, 1)); +#endif + return rval; +} + static PyObject * quote_unescaped_unicode(PyObject *pystr) { @@ -259,7 +278,8 @@ ascii_escape_unicode(PyObject *pystr) } if (output_size == input_chars + 2) { - return quote_unescaped_unicode(pystr); + /* No escaping needed, so every character is printable ASCII */ + return quote_unescaped_ascii(pystr); } return ascii_escape_unicode_and_size(input, kind, input_chars, output_size); From 6823cb4a48ceb986ec7b483759c40ddd178cf369 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 30 Aug 2026 22:09:35 +0200 Subject: [PATCH 4/4] Apply suggestion from @eendebakpt --- .../next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst b/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst index dfe82854fa24ed1..2720ab5b8df1809 100644 --- a/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst +++ b/Misc/NEWS.d/next/Library/2026-08-13-09-00-00.gh-issue-156241.eScFP1.rst @@ -1 +1 @@ -Speed up :func:`json.dump` for strings that need no escaping. +Speed up :func:`json.dump` and :func:`json.dumps` for strings that need no escaping.