https://github.com/python/cpython/commit/43a4511b6e62437048366893499768f9130dad3b
commit: 43a4511b6e62437048366893499768f9130dad3b
branch: main
author: Armaan Sandhu <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-29T14:15:15Z
summary:

gh-159503: Avoid copying the unsent remainder of a bytes write in asyncio 
selector transports (#159504)

files:
A Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst
M Doc/library/asyncio-protocol.rst
M Lib/asyncio/selector_events.py
M Lib/test/test_asyncio/test_selector_events.py

diff --git a/Doc/library/asyncio-protocol.rst b/Doc/library/asyncio-protocol.rst
index 58f77feb311984..42c2d509d5a729 100644
--- a/Doc/library/asyncio-protocol.rst
+++ b/Doc/library/asyncio-protocol.rst
@@ -333,6 +333,9 @@ Write-only Transports
    This method does not block; it buffers the data and arranges for it
    to be sent out asynchronously.
 
+   The transport keeps a reference to *data* rather than copying. *data*
+   must not be mutated while being sent.
+
 .. method:: WriteTransport.writelines(list_of_data)
 
    Write a list (or any iterable) of data bytes to the transport.
diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py
index 57ba2f13246c94..15a3754262d078 100644
--- a/Lib/asyncio/selector_events.py
+++ b/Lib/asyncio/selector_events.py
@@ -1099,7 +1099,9 @@ def write(self, data):
             # Not all was written; register write handler.
             self._add_writer(self._sock_fd, self._write_ready)
 
-        # Add it to the buffer.
+        # Add it to the buffer as a view, so partial sends don't copy it.
+        if not isinstance(data, memoryview):
+            data = memoryview(data)
         self._buffer.append(data)
         self._buffer_size += len(data)
         self._maybe_pause_protocol()
diff --git a/Lib/test/test_asyncio/test_selector_events.py 
b/Lib/test/test_asyncio/test_selector_events.py
index 74322105a7c757..e080225bd976d9 100644
--- a/Lib/test/test_asyncio/test_selector_events.py
+++ b/Lib/test/test_asyncio/test_selector_events.py
@@ -855,14 +855,23 @@ def test_write_partial_none(self):
         self.assertEqual(list_to_buffer([b'data']), transport._buffer)
 
     def test_write_tryagain(self):
-        self.sock.send.side_effect = BlockingIOError
-
         data = b'data'
-        transport = self.socket_transport()
-        transport.write(data)
-
-        self.loop.assert_writer(7, transport._write_ready)
-        self.assertEqual(list_to_buffer([b'data']), transport._buffer)
+        for buf in data, bytearray(data), memoryview(data):
+            with self.subTest(type=type(buf).__name__):
+                self.sock.send.side_effect = BlockingIOError
+
+                transport = self.socket_transport()
+                transport.write(buf)
+
+                self.loop.assert_writer(7, transport._write_ready)
+                self.assertEqual(list_to_buffer([b'data']), transport._buffer)
+                # gh-159503: the data is buffered as a view, not a copy
+                self.assertIsInstance(transport._buffer[0], memoryview)
+                self.assertIs(transport._buffer[0].obj, memoryview(buf).obj)
+                if isinstance(buf, bytearray):
+                    # The buffered view pins the bytearray against resizing.
+                    with self.assertRaises(BufferError):
+                        buf.append(0)
 
     def test_write_sendmsg_no_data(self):
         self.sock.sendmsg = mock.Mock()
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst 
b/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst
new file mode 100644
index 00000000000000..158968d0c2a1ce
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst
@@ -0,0 +1,3 @@
+Fix quadratic copying in :mod:`asyncio` selector socket transports on partial
+sends. A :class:`bytearray` passed to :meth:`~asyncio.WriteTransport.write`
+now stays locked against resizing until it has been sent.

_______________________________________________
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