https://github.com/python/cpython/commit/121e27c484539faaf24e6b264846217a8f4b9738
commit: 121e27c484539faaf24e6b264846217a8f4b9738
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-12T20:22:01Z
summary:

gh-157242: Add _PyBytes_IsMutable() assertion (#157371)

Elaborate on mutability in _PyBytes_Resize() and PyBytesWriter
documentation.

Check _PyBytes_IsMutable() in functions which require a mutable bytes
object like PyBytesWriter_Resize().

files:
M Doc/c-api/bytes.rst
M Include/internal/pycore_bytesobject.h
M Objects/bytearrayobject.c
M Objects/bytesobject.c

diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst
index bf89481a628ed2..60a90b3f096912 100644
--- a/Doc/c-api/bytes.rst
+++ b/Doc/c-api/bytes.rst
@@ -231,6 +231,7 @@ called with a non-bytes parameter.
    Resize a bytes object. *newsize* will be the new length of the bytes object.
    You can think of it as creating a new bytes object and destroying the old
    one, only more efficiently.
+
    Pass the address of an
    existing bytes object as an lvalue (it may be written into), and the new 
size
    desired.  On success, *\*bytes* holds the resized bytes object and ``0`` is
@@ -239,6 +240,11 @@ called with a non-bytes parameter.
    *\*bytes* is set to ``NULL``, :exc:`MemoryError` is set, and ``-1`` is
    returned.
 
+   While bytes objects are usually immutable in Python, this special C API
+   allows mutating a bytes object in-place. The returned bytes object can still
+   be mutated using :c:func:`PyBytesWriter_GetData`; except if *newsize* is
+   zero in which case it returns the immutable empty bytes string.
+
    .. soft-deprecated:: 3.15
       Use the :c:type:`PyBytesWriter` API instead.
 
@@ -290,10 +296,10 @@ object.
 
 .. c:type:: PyBytesWriter
 
-   A bytes writer instance.
+   A bytes writer object.
 
-   The API is **not thread safe**: a writer should only be used by a single
-   thread at the same time.
+   The API is **not thread safe**. A :c:type:`PyBytesWriter` object must only
+   be used by a single thread, it must not be shared between threads.
 
    The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on
    success, or :c:func:`PyBytesWriter_Discard` on error.
diff --git a/Include/internal/pycore_bytesobject.h 
b/Include/internal/pycore_bytesobject.h
index 32da177c637c26..443bdb26ff8738 100644
--- a/Include/internal/pycore_bytesobject.h
+++ b/Include/internal/pycore_bytesobject.h
@@ -77,6 +77,10 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, 
Py_ssize_t n);
 
 extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);
 
+#ifndef NDEBUG
+extern int _PyBytes_IsMutable(PyObject *obj);
+#endif
+
 /* --- PyBytesWriter ------------------------------------------------------ */
 
 struct PyBytesWriter {
diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c
index 05e1b27dc82558..de30c6118ba176 100644
--- a/Objects/bytearrayobject.c
+++ b/Objects/bytearrayobject.c
@@ -256,6 +256,7 @@ bytearray_resize_storage(PyByteArrayObject *self,
         bytearray_write_trailing_null_byte(self);
         return -1;
     }
+    assert(_PyBytes_IsMutable(self->ob_bytes_object));
     return 0;
 }
 
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index deddb8d959b157..080b53e088796f 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -37,7 +37,7 @@ static Py_ssize_t 
_PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer);
 
 #define CHARACTERS _Py_SINGLETON(bytes_characters)
 #define CHARACTER(ch) \
-     ((PyBytesObject *)&(CHARACTERS[ch]));
+     ((PyBytesObject *)&(CHARACTERS[ch]))
 #define EMPTY (&_Py_SINGLETON(bytes_empty))
 
 
@@ -3294,6 +3294,29 @@ PyBytes_ConcatAndDel(PyObject **pv, PyObject *w)
 }
 
 
+#ifndef NDEBUG
+// Make sure that a bytes object can still be mutated.
+//
+// Usage: assert(_PyBytes_IsMutable(obj)).
+int
+_PyBytes_IsMutable(PyObject *v)
+{
+    // Singleton objects must never be modified
+    assert(!_Py_IsImmortal(v));
+
+    Py_ssize_t size = PyBytes_GET_SIZE(v);
+    if (size == 0) {
+        assert(v != bytes_get_empty());
+    }
+    else if (size == 1) {
+        unsigned char ch = PyBytes_AS_STRING(v)[0];
+        assert(v != (PyObject*)CHARACTER(ch));
+    }
+    return 1;
+}
+#endif
+
+
 /* The following function breaks the notion that bytes are immutable:
    it changes the size of a bytes object.  You can think of it
    as creating a new bytes object and destroying the old one, only
@@ -3331,6 +3354,7 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t 
newsize)
         }
         *pv = result;
         Py_DECREF(v);
+        assert(_PyBytes_IsMutable(*pv));
         return 0;
     }
 
@@ -3352,9 +3376,12 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t 
newsize)
                Py_MIN(oldsize, newsize));
         *pv = result;
         Py_DECREF(v);
+        assert(_PyBytes_IsMutable(*pv));
         return 0;
     }
-    assert(v != bytes_get_empty());
+
+    // Only mutable bytes can be resized in-place
+    assert(_PyBytes_IsMutable(v));
 
     if ((size_t)newsize > (size_t)PY_SSIZE_T_MAX - PyBytesObject_SIZE) {
         PyErr_SetString(PyExc_OverflowError,
@@ -3385,6 +3412,7 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t 
newsize)
     Py_SET_SIZE(sv, newsize);
     sv->ob_sval[newsize] = '\0';
     set_ob_shash(sv, -1);          /* invalidate cached hash value */
+    assert(_PyBytes_IsMutable(*pv));
     return 0;
 }
 
@@ -3647,6 +3675,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
size, int resize)
                 assert(writer->obj != NULL);
                 return -1;
             }
+            assert(_PyBytes_IsMutable(writer->obj));
         }
         assert(writer->obj != NULL);
     }
@@ -3673,6 +3702,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
size, int resize)
                    writer->small_buffer,
                    sizeof(writer->small_buffer));
         }
+        assert(_PyBytes_IsMutable(writer->obj));
     }
 
 #ifdef Py_DEBUG

_______________________________________________
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