From a8b8baef307211e1bfa6705a112e46e08b110f80 Mon Sep 17 00:00:00 2001 From: Dmitry Voropaev Date: Thu, 24 Sep 2026 23:22:23 +0300 Subject: [PATCH 1/2] gh-133492: Bring typing.IO's write signatures closer to typeshed BinaryIO.write was annotated as taking only bytes or bytearray, but the binary streams open() returns take any object with a buffer: BytesIO.write accepts a memoryview or an array.array just fine. typeshed spells this as ReadableBuffer on an IO[bytes] self-type overload, which a runtime stub cannot express, so the closest it can get is collections.abc.Buffer. writelines was off along two axes. IO.writelines said list[AnyStr] where the real method takes any iterable, and where typeshed says Iterable[AnyStr]. On the binary side it then stayed at bytes, although BytesIO.writelines swallows a list of memoryviews as readily as write swallows one memoryview; typeshed has a second self-type overload for exactly that, so BinaryIO now overrides writelines the way it already overrides write. Type checkers read typeshed rather than these annotations, so nothing about type checking changes; the runtime stubs just stop contradicting the stubs people check against. --- Lib/test/test_typing.py | 15 +++++++++++++++ Lib/typing.py | 10 ++++++++-- ...2026-09-24-23-22-01.gh-issue-133492.-bKR-v.rst | 5 +++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-24-23-22-01.gh-issue-133492.-bKR-v.rst diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index b0d468b34092b25..9939216a98e826a 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -9660,6 +9660,21 @@ def stuff(a: BinaryIO) -> bytes: a = stuff.__annotations__['a'] self.assertEqual(a.__parameters__, ()) + def test_write_annotations(self): + # These are evaluated lazily, so a wrong name in any of them would + # stay invisible until something introspects the stub. Compare the + # whole mapping so that a dropped or stray annotation shows up too. + self.assertEqual( + IO.writelines.__annotations__, + {'lines': collections.abc.Iterable[AnyStr], 'return': None}) + self.assertEqual( + BinaryIO.write.__annotations__, + {'s': collections.abc.Buffer, 'return': int}) + self.assertEqual( + BinaryIO.writelines.__annotations__, + {'lines': collections.abc.Iterable[collections.abc.Buffer], + 'return': None}) + class RETests(BaseTestCase): # Much of this is really testing _TypeAlias. diff --git a/Lib/typing.py b/Lib/typing.py index 99c467a8af07d89..a1ee7693519656f 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3636,7 +3636,7 @@ def write(self, s: AnyStr, /) -> int: pass @abstractmethod - def writelines(self, lines: list[AnyStr], /) -> None: + def writelines(self, lines: collections.abc.Iterable[AnyStr], /) -> None: pass @abstractmethod @@ -3654,7 +3654,13 @@ class BinaryIO(IO[bytes]): __slots__ = () @abstractmethod - def write(self, s: bytes | bytearray, /) -> int: + def write(self, s: collections.abc.Buffer, /) -> int: + pass + + @abstractmethod + def writelines( + self, lines: collections.abc.Iterable[collections.abc.Buffer], / + ) -> None: pass @abstractmethod diff --git a/Misc/NEWS.d/next/Library/2026-09-24-23-22-01.gh-issue-133492.-bKR-v.rst b/Misc/NEWS.d/next/Library/2026-09-24-23-22-01.gh-issue-133492.-bKR-v.rst new file mode 100644 index 000000000000000..8f296aef800d2c5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-24-23-22-01.gh-issue-133492.-bKR-v.rst @@ -0,0 +1,5 @@ +:class:`typing.BinaryIO` now annotates ``write()`` and ``writelines()`` in +terms of :class:`collections.abc.Buffer` rather than :class:`bytes`, and +``writelines()`` on :class:`typing.IO` takes any iterable rather than a list. +Binary streams accept all of this already, and typeshed says the same through +overloads that a runtime stub has no way to spell. From c209802468737714c6e6dc487fd97dad57aaf197 Mon Sep 17 00:00:00 2001 From: Dmitry Voropaev Date: Thu, 24 Sep 2026 23:25:01 +0300 Subject: [PATCH 2/2] gh-133492: Declare readinto() on typing.BinaryIO The method is present on, or reachable from, every class typeshed marks as inheriting from BinaryIO. There are seven of them: _io.FileIO, BytesIO, BufferedReader, BufferedWriter and BufferedRandom pick it up from the raw and buffered base classes, http.client.HTTPResponse from io.BufferedIOBase, and codecs.StreamRecoder reaches it through __getattr__. StreamRecoder is not much of an exception there. typeshed already declares nine methods on it that the class itself does not have (close, fileno, flush, isatty, readable, truncate, seekable, tell and writable), with a comment saying they are delegated through __getattr__. Wrap a stream that has only read and write, and all nine raise AttributeError, exactly as readinto does. The return type is int rather than int | None for the same reason IO.read is AnyStr: a raw stream in non-blocking mode can return None from either, and these stubs already pass over that case. --- Doc/library/typing.rst | 4 ++++ Lib/test/test_typing.py | 8 ++++++++ Lib/typing.py | 4 ++++ .../2026-09-24-23-23-48.gh-issue-133492.9Yd4xo.rst | 4 ++++ 4 files changed, 20 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-24-23-23-48.gh-issue-133492.9Yd4xo.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index c909b8bad6d726c..33d07623592ee9d 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -3027,6 +3027,10 @@ ABCs and Protocols for working with I/O :func:`open`. Please note that these classes are not protocols, and their interface is fairly broad. + .. versionchanged:: next + ``BinaryIO`` now declares a ``readinto()`` method, matching the binary + streams that :func:`open` returns. + The protocols :class:`io.Reader` and :class:`io.Writer` offer a simpler alternative for argument types, when only the ``read()`` or ``write()`` methods are accessed, respectively:: diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 9939216a98e826a..c559a863985fb1e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -9675,6 +9675,14 @@ def test_write_annotations(self): {'lines': collections.abc.Iterable[collections.abc.Buffer], 'return': None}) + def test_binaryio_readinto(self): + # Every class typeshed marks as a BinaryIO has readinto(), so the + # runtime stub declares it as well. + self.assertTrue(BinaryIO.readinto.__isabstractmethod__) + self.assertEqual( + BinaryIO.readinto.__annotations__, + {'buffer': collections.abc.Buffer, 'return': int}) + class RETests(BaseTestCase): # Much of this is really testing _TypeAlias. diff --git a/Lib/typing.py b/Lib/typing.py index a1ee7693519656f..d6a1f958bb87052 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3653,6 +3653,10 @@ class BinaryIO(IO[bytes]): __slots__ = () + @abstractmethod + def readinto(self, buffer: collections.abc.Buffer, /) -> int: + pass + @abstractmethod def write(self, s: collections.abc.Buffer, /) -> int: pass diff --git a/Misc/NEWS.d/next/Library/2026-09-24-23-23-48.gh-issue-133492.9Yd4xo.rst b/Misc/NEWS.d/next/Library/2026-09-24-23-23-48.gh-issue-133492.9Yd4xo.rst new file mode 100644 index 000000000000000..63b196791c1a614 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-24-23-23-48.gh-issue-133492.9Yd4xo.rst @@ -0,0 +1,4 @@ +:class:`typing.BinaryIO` now declares a ``readinto()`` method. Every class +that typeshed marks as inheriting from ``BinaryIO`` provides it, in +:class:`codecs.StreamRecoder`'s case through ``__getattr__``, so the runtime +stub was missing a method the type describes.