From 5a1e0ad8c1b0d734b231d6c34d7e51c682b76654 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 18 Mar 2026 18:52:08 +0200 Subject: [PATCH 1/3] gh-146056: Fix list.__repr__() for lists containing NULLs --- Lib/test/test_capi/test_list.py | 4 ++++ .../2026-03-18-18-52-00.gh-issue-146056.r1tVSo.rst | 1 + Objects/listobject.c | 2 +- Objects/unicode_writer.c | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-03-18-18-52-00.gh-issue-146056.r1tVSo.rst diff --git a/Lib/test/test_capi/test_list.py b/Lib/test/test_capi/test_list.py index 67ed5d0b4f8722..794a1a3aebf720 100644 --- a/Lib/test/test_capi/test_list.py +++ b/Lib/test/test_capi/test_list.py @@ -350,6 +350,10 @@ def test_list_extend(self): # CRASHES list_extend(NULL, []) # CRASHES list_extend([], NULL) + def test_incomplete_list_repr(self): + lst = _testlimitedcapi.list_new(3) + self.assertEqual(repr(lst), '[, , ]') + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-18-18-52-00.gh-issue-146056.r1tVSo.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-18-18-52-00.gh-issue-146056.r1tVSo.rst new file mode 100644 index 00000000000000..67502657047dee --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-18-18-52-00.gh-issue-146056.r1tVSo.rst @@ -0,0 +1 @@ +Fix :meth:`!list.__repr__` for lists containing ``NULL``\ s. diff --git a/Objects/listobject.c b/Objects/listobject.c index 1cc62764e2fd8c..654b8130e70840 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -601,7 +601,7 @@ list_repr_impl(PyListObject *v) so must refetch the list size on each iteration. */ for (Py_ssize_t i = 0; i < Py_SIZE(v); ++i) { /* Hold a strong reference since repr(item) can mutate the list */ - item = Py_NewRef(v->ob_item[i]); + item = Py_XNewRef(v->ob_item[i]); if (i > 0) { if (PyUnicodeWriter_WriteChar(writer, ',') < 0) { diff --git a/Objects/unicode_writer.c b/Objects/unicode_writer.c index 2b944bf1ea8cde..cd2688e32dfaf8 100644 --- a/Objects/unicode_writer.c +++ b/Objects/unicode_writer.c @@ -383,6 +383,10 @@ PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) int PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) { + if (obj == NULL) { + return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, "", 6); + } + if (Py_TYPE(obj) == &PyLong_Type) { return _PyLong_FormatWriter((_PyUnicodeWriter*)writer, obj, 10, 0); } From a75c4c13fdf75abecbe3f95e846b5749d6a866ee Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 18 Mar 2026 20:36:01 +0200 Subject: [PATCH 2/3] Add more tests and update docs. Co-authored-by: Victor Stinner --- Doc/c-api/file.rst | 7 +++++-- Doc/c-api/object.rst | 8 ++++++++ Doc/c-api/unicode.rst | 2 ++ Lib/test/test_capi/test_list.py | 2 +- Lib/test/test_capi/test_tuple.py | 5 +++++ Lib/test/test_capi/test_unicode.py | 7 +++++++ .../C_API/2026-03-18-20-18-59.gh-issue-146056.nnZIgp.rst | 1 + Modules/_testcapi/unicode.c | 1 + 8 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-03-18-20-18-59.gh-issue-146056.nnZIgp.rst diff --git a/Doc/c-api/file.rst b/Doc/c-api/file.rst index 0580e4c8f79db0..afa6a0b5bf92c6 100644 --- a/Doc/c-api/file.rst +++ b/Doc/c-api/file.rst @@ -123,9 +123,12 @@ the :mod:`io` APIs instead. Write object *obj* to file object *p*. The only supported flag for *flags* is :c:macro:`Py_PRINT_RAW`; if given, the :func:`str` of the object is written - instead of the :func:`repr`. Return ``0`` on success or ``-1`` on failure; the - appropriate exception will be set. + instead of the :func:`repr`. + + If *obj* is ``NULL``, write string ``""``. + Return ``0`` on success or ``-1`` on failure; the + appropriate exception will be set. .. c:function:: int PyFile_WriteString(const char *s, PyObject *p) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 15a4b55eab82f0..a5c1c287b90d58 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -363,6 +363,8 @@ Object Protocol representation on success, ``NULL`` on failure. This is the equivalent of the Python expression ``repr(o)``. Called by the :func:`repr` built-in function. + If argument is ``NULL``, return string ``''``. + .. versionchanged:: 3.4 This function now includes a debug assertion to help ensure that it does not silently discard an active exception. @@ -377,6 +379,8 @@ Object Protocol a string similar to that returned by :c:func:`PyObject_Repr` in Python 2. Called by the :func:`ascii` built-in function. + If argument is ``NULL``, return string ``''``. + .. index:: string; PyObject_Str (C function) @@ -387,6 +391,8 @@ Object Protocol Python expression ``str(o)``. Called by the :func:`str` built-in function and, therefore, by the :func:`print` function. + If argument is ``NULL``, return string ``''``. + .. versionchanged:: 3.4 This function now includes a debug assertion to help ensure that it does not silently discard an active exception. @@ -402,6 +408,8 @@ Object Protocol a TypeError is raised when *o* is an integer instead of a zero-initialized bytes object. + If argument is ``NULL``, return :class:`bytes` object ``b''``. + .. c:function:: int PyObject_IsSubclass(PyObject *derived, PyObject *cls) diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst index 4845e0f300278d..4fdabb7a77f955 100644 --- a/Doc/c-api/unicode.rst +++ b/Doc/c-api/unicode.rst @@ -1887,6 +1887,8 @@ object. Call :c:func:`PyObject_Repr` on *obj* and write the output into *writer*. + If *obj* is ``NULL``, write string ``""`` into *writer*. + On success, return ``0``. On error, set an exception, leave the writer unchanged, and return ``-1``. diff --git a/Lib/test/test_capi/test_list.py b/Lib/test/test_capi/test_list.py index 794a1a3aebf720..b95b8ba960bd8b 100644 --- a/Lib/test/test_capi/test_list.py +++ b/Lib/test/test_capi/test_list.py @@ -350,7 +350,7 @@ def test_list_extend(self): # CRASHES list_extend(NULL, []) # CRASHES list_extend([], NULL) - def test_incomplete_list_repr(self): + def test_uninitialized_list_repr(self): lst = _testlimitedcapi.list_new(3) self.assertEqual(repr(lst), '[, , ]') diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index 0c27e81168ff77..d497ba30b001ac 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -73,6 +73,7 @@ def test_tuple_new(self): self.assertRaises(SystemError, tuple_new, PY_SSIZE_T_MIN) self.assertRaises(MemoryError, tuple_new, PY_SSIZE_T_MAX) + def test_tuple_fromarray(self): # Test PyTuple_FromArray() tuple_fromarray = _testcapi.tuple_fromarray @@ -357,6 +358,10 @@ def my_iter(): self.assertEqual(tuple(my_iter()), (TAG, *range(10))) self.assertEqual(tuples, []) + def test_uninitialized_tuple_repr(self): + tup = _testlimitedcapi.tuple_new(3) + self.assertEqual(repr(tup), '(, , )') + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_capi/test_unicode.py b/Lib/test/test_capi/test_unicode.py index 6a9c60f3a6d75e..55120448a8a3ec 100644 --- a/Lib/test/test_capi/test_unicode.py +++ b/Lib/test/test_capi/test_unicode.py @@ -1779,6 +1779,13 @@ def test_basic(self): self.assertEqual(writer.finish(), "var=long value 'repr'") + def test_repr_null(self): + writer = self.create_writer(0) + writer.write_utf8(b'var=', -1) + writer.write_repr(NULL) + self.assertEqual(writer.finish(), + "var=") + def test_utf8(self): writer = self.create_writer(0) writer.write_utf8(b"ascii", -1) diff --git a/Misc/NEWS.d/next/C_API/2026-03-18-20-18-59.gh-issue-146056.nnZIgp.rst b/Misc/NEWS.d/next/C_API/2026-03-18-20-18-59.gh-issue-146056.nnZIgp.rst new file mode 100644 index 00000000000000..7c5fc7a0538e21 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-03-18-20-18-59.gh-issue-146056.nnZIgp.rst @@ -0,0 +1 @@ +:c:func:`PyUnicodeWriter_WriteRepr` now supports ``NULL`` argument. diff --git a/Modules/_testcapi/unicode.c b/Modules/_testcapi/unicode.c index 203282dd53dd0a..668adc5085b4fe 100644 --- a/Modules/_testcapi/unicode.c +++ b/Modules/_testcapi/unicode.c @@ -449,6 +449,7 @@ writer_write_repr(PyObject *self_raw, PyObject *args) if (!PyArg_ParseTuple(args, "O", &obj)) { return NULL; } + NULLABLE(obj); if (PyUnicodeWriter_WriteRepr(self->writer, obj) < 0) { return NULL; From 9190ea48521ea87d1affaccb514c2536cb3dd529 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 18 Mar 2026 23:23:44 +0200 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Victor Stinner --- Doc/c-api/file.rst | 2 +- Doc/c-api/object.rst | 8 ++++---- Doc/c-api/unicode.rst | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/c-api/file.rst b/Doc/c-api/file.rst index afa6a0b5bf92c6..d89072ab24e241 100644 --- a/Doc/c-api/file.rst +++ b/Doc/c-api/file.rst @@ -125,7 +125,7 @@ the :mod:`io` APIs instead. :c:macro:`Py_PRINT_RAW`; if given, the :func:`str` of the object is written instead of the :func:`repr`. - If *obj* is ``NULL``, write string ``""``. + If *obj* is ``NULL``, write the string ``""``. Return ``0`` on success or ``-1`` on failure; the appropriate exception will be set. diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index a5c1c287b90d58..eedeb180c6b760 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -363,7 +363,7 @@ Object Protocol representation on success, ``NULL`` on failure. This is the equivalent of the Python expression ``repr(o)``. Called by the :func:`repr` built-in function. - If argument is ``NULL``, return string ``''``. + If argument is ``NULL``, return the string ``''``. .. versionchanged:: 3.4 This function now includes a debug assertion to help ensure that it @@ -379,7 +379,7 @@ Object Protocol a string similar to that returned by :c:func:`PyObject_Repr` in Python 2. Called by the :func:`ascii` built-in function. - If argument is ``NULL``, return string ``''``. + If argument is ``NULL``, return the string ``''``. .. index:: string; PyObject_Str (C function) @@ -391,7 +391,7 @@ Object Protocol Python expression ``str(o)``. Called by the :func:`str` built-in function and, therefore, by the :func:`print` function. - If argument is ``NULL``, return string ``''``. + If argument is ``NULL``, return the string ``''``. .. versionchanged:: 3.4 This function now includes a debug assertion to help ensure that it @@ -408,7 +408,7 @@ Object Protocol a TypeError is raised when *o* is an integer instead of a zero-initialized bytes object. - If argument is ``NULL``, return :class:`bytes` object ``b''``. + If argument is ``NULL``, return the :class:`bytes` object ``b''``. .. c:function:: int PyObject_IsSubclass(PyObject *derived, PyObject *cls) diff --git a/Doc/c-api/unicode.rst b/Doc/c-api/unicode.rst index 4fdabb7a77f955..7e9346ea9f7c6d 100644 --- a/Doc/c-api/unicode.rst +++ b/Doc/c-api/unicode.rst @@ -1887,7 +1887,7 @@ object. Call :c:func:`PyObject_Repr` on *obj* and write the output into *writer*. - If *obj* is ``NULL``, write string ``""`` into *writer*. + If *obj* is ``NULL``, write the string ``""`` into *writer*. On success, return ``0``. On error, set an exception, leave the writer unchanged, and return ``-1``.