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]