json.load() Crashes on a Non-Blocking None (tested)
A non-blocking raw stream signals "no data ready yet" by returning None from readinto() -- distinct from the empty read that means real end-of-stream. We measured that json.load() does exactly one thing with its file argument: call fp.read() and hand the result straight to json.loads(), with no retry and no check for None. When the underlying read returns a transient None, json.loads(None) crashes with TypeError: the JSON object must be str, bytes or bytearray, not NoneType -- an error that looks like you passed the wrong type, not that a non-blocking stream had a retryable moment, on a document that is valid and complete one read call away.
1. A real, valid JSON document behind one transient None
import io, json
class FlakyRaw(io.RawIOBase):
def __init__(self, payload):
self.calls = 0
self.sent = False
self.payload = payload
def readable(self):
return True
def readinto(self, b):
self.calls += 1
if self.calls == 1:
return None # transient: no data ready yet
if not self.sent:
self.sent = True
n = len(self.payload)
b[:n] = self.payload
return n # the real, complete JSON document
return 0 # real EOF after that
payload = b'{"key": "value"}'
raw = FlakyRaw(payload)
br = io.BufferedReader(raw)
try:
data = json.load(br)
print("data:", data)
except Exception as e:
print("CRASHED:", type(e).__name__, e)
print("raw calls made:", raw.calls)
CRASHED: TypeError the JSON object must be str, bytes or bytearray, not NoneType
raw calls made: 1
Measured: the document is valid and complete, one read call away. json.load()'s entire implementation is return loads(fp.read()) -- a single, unretried call. When fp.read() returns None, loads() raises a TypeError that names the wrong problem: it looks like a type mismatch in your code, not a non-blocking stream signal.
2. The fix: drain into a real buffer first, retrying on None
def drain_retrying_on_none(raw_stream, chunk_size=65536):
out = io.BytesIO()
br = io.BufferedReader(raw_stream)
while True:
chunk = br.read(chunk_size)
if chunk is None:
continue # retry: not ready yet, not EOF
if not chunk:
break # real EOF
out.write(chunk)
out.seek(0)
return out
raw2 = FlakyRaw(payload)
safe_buf = drain_retrying_on_none(raw2)
print("safe data:", json.load(safe_buf))
safe data: {'key': 'value'}
Measured: resolving the None/EOF distinction before the bytes reach json.load() parses the document correctly. If you ever see this exact error on a file object you're confident is binary- or text-mode correct, the real cause is almost certainly a non-blocking stream, not a type mistake.
Frequently asked questions
Is this documented behavior?
The json module's documentation for load() says fp must be a .read()-supporting file-like object containing a JSON document; it says nothing about None or non-blocking streams. We measured that a None-returning source triggers the same TypeError json.loads() uses for a genuine type mistake, like accidentally passing an integer or a list.
How can I tell the real type mistake from this one?
We measured that load()'s implementation is just loads(fp.read()) with no intermediate step -- if fp is a file object you opened yourself (not some other value you're passing by mistake), and the TypeError specifically says 'not NoneType', the file's read() call returned None, which almost always means a non-blocking source rather than your own type error.
Does this affect json.loads() directly?
No -- loads() takes the string/bytes data directly, not a file object, so there is no read() call involved. This specific failure mode only applies to load(), which performs that read() call internally.
Is this related to the other None/EOF findings?
Yes -- same family already confirmed in shutil.copyfileobj, gzip/bz2/lzma, tarfile/zipfile, TextIOWrapper.readline(), ElementTree.parse(), tomllib.load(), email.message_from_binary_file(), plistlib.load(), wave.open(), http.client.HTTPResponse, smtplib.getreply(), and poplib._getline(): a correctly-returned non-blocking None gets mishandled one layer above io itself. json.load()'s one-shot, no-retry implementation makes it one of the simplest and most direct examples of the pattern -- and likely the most common one developers will actually hit.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in json.load(), reading a genuinely valid, complete JSON document through an io.BufferedReader over a custom non-blocking RawIOBase whose readinto() returns None once before returning the real JSON bytes. PlainHeaders is our extension; Python's json and io modules are built in.