io.BufferedReader.read1() Turns a Non-Blocking None Into b'' (tested)
A non-blocking raw stream signals "no data available right now" by having readinto() return None -- distinct from the empty read that means real end-of-stream. io.BufferedReader.read() handles this correctly: called on a stream with nothing ready, it returns None itself, so a caller can tell "try again later" apart from "the stream is done." read1(), meant for exactly this kind of lower-level non-blocking use, does not: the same transient None comes back from read1() as b'' -- the identical empty-bytes value Python's read conventions use for real EOF. A caller using the standard while chunk := f.read1(n): process(chunk) idiom stops permanently on the first moment the stream isn't instantly ready, even though more data is still coming.
1. read() correctly distinguishes 'not ready yet' from EOF
import io
class FlakyRaw(io.RawIOBase):
def __init__(self):
self.calls = 0
def readable(self):
return True
def readinto(self, b):
self.calls += 1
if self.calls == 1:
return None # transient: no data ready yet
data = b"hello world"
n = min(len(data), len(b))
b[:n] = data[:n]
return n
raw = FlakyRaw()
br = io.BufferedReader(raw, buffer_size=64)
print("read(5) while not ready:", repr(br.read(5)))
read(5) while not ready: None
Measured: read() propagates the raw stream's None signal as-is. A caller can check if chunk is None: retry_later() and correctly keep the connection open.
2. read1() turns the exact same signal into b'', identical to real EOF
raw2 = FlakyRaw()
br2 = io.BufferedReader(raw2, buffer_size=64)
r1 = br2.read1(5)
print("first read1() call (raw said None):", repr(r1))
r2 = br2.read1(5)
print("second read1() call (raw has real data now):", repr(r2))
first read1() call (raw said None): b''
second read1() call (raw has real data now): b'hello'
Measured: the first read1() call returns b'' -- exactly what a real end-of-stream would return -- even though the very next call proves the stream was never at EOF at all. Standard Python code that checks if not chunk: break cannot tell these two situations apart through read1().
3. The fix: use read(), not read1(), on streams that can return None
raw3 = FlakyRaw()
br3 = io.BufferedReader(raw3, buffer_size=64)
chunk = br3.read(5)
while chunk is None:
chunk = br3.read(5) # retry on None, distinct from stopping on b''
print("correct result after retrying on None:", repr(chunk))
correct result after retrying on None: b'hello'
Measured: checking specifically for None (not just falsy/empty) and retrying only in that case, using read() rather than read1(), reads the real data correctly instead of stopping early.
Frequently asked questions
Is read1() documented to behave this way?
The documentation describes read1() as making at most one call to the underlying raw stream and says little about its behavior under non-blocking None specifically; we measured that it collapses None into b'', while read() on the same BufferedReader, wrapping the same raw stream, keeps None distinguishable from b''.
Why would read1() be more affected than read()?
We measured that read1()'s one-call-only contract means it has no fallback path to distinguish 'this one call returned nothing because of None' from 'this one call returned nothing because of EOF' -- both just become an empty result as far as read1()'s return value is concerned.
Does this affect TextIOWrapper too?
We only measured BufferedReader.read1() directly against BufferedReader.read(); a related but different bug already exists one layer up -- TextIOWrapper.read() crashes with TypeError on the same None signal instead of handling it at all, which is a separate, already-documented finding.
What's the safe general rule here?
We measured that only read() reliably preserves the None/EOF distinction on a non-blocking BufferedReader. If you need read1()'s one-call behavior for latency reasons, you cannot safely use its return value alone to detect EOF on a stream that can also return None.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in io.BufferedReader wrapping a custom non-blocking RawIOBase whose readinto() returns None on its first call (no data ready yet) and real data on the next, comparing read() against read1(). PlainHeaders is our extension; Python's io module is built in.