wave.open() 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 wave.open()'s underlying RIFF-chunk reader stores whatever its first read call returns as the chunk name and immediately calls len() on it with no None check -- so the transient None crashes with TypeError: object of type 'NoneType' has no len(), an error that looks like an internal library bug rather than anything related to stream readiness, on a WAV file that is valid and complete one read call away.
1. A real, valid WAV file behind one transient None
import io, wave
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 WAV file
return 0 # real EOF after that
buf = io.BytesIO()
w = wave.open(buf, "wb")
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(8000)
w.writeframes(b"\x00\x01" * 100)
w.close()
payload = buf.getvalue()
raw = FlakyRaw(payload)
br = io.BufferedReader(raw)
try:
r = wave.open(br, "rb")
print("nframes:", r.getnframes())
except Exception as e:
print("CRASHED:", type(e).__name__, e)
print("raw calls made:", raw.calls)
CRASHED: TypeError object of type 'NoneType' has no len()
raw calls made: 1
Measured: the WAV file is valid and complete, one read call away. wave.open()'s internal _Chunk.__init__ reads 4 bytes expecting the RIFF chunk name, stores whatever came back, and calls len() on it unconditionally -- when that read returns None, the len() call itself crashes with a TypeError that names an unrelated internal attribute, not the real cause.
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)
r2 = wave.open(safe_buf, "rb")
print("nframes:", r2.getnframes())
print("frames read:", len(r2.readframes(100)))
nframes: 100
frames read: 200
Measured: resolving the None/EOF distinction before the bytes reach wave.open() reads the header and frames correctly.
Frequently asked questions
Is this documented behavior?
The wave module's documentation for open() says nothing about non-blocking streams or a None read result; it assumes a blocking file object. We measured that a None-returning source triggers a TypeError whose message points at an internal chunk-name attribute, not at the real cause.
Does aifc have the same problem?
aifc.py was removed from the standard library in Python 3.13 (deprecated since 3.11), so we could not measure it on this version; it was built on the same underlying _aifc_params / chunk-reading approach as wave in earlier versions, so the same class of bug is plausible there too for anyone still on an older Python.
How can I tell this apart from a genuinely corrupt WAV file?
A real corrupt file typically fails with a different message (for example a mismatched RIFF header once a chunk name is actually read). We measured that this specific TypeError, naming NoneType and len(), only appears when the underlying read returned None -- if you're reading from a non-blocking source, suspect the retry signal before suspecting the file.
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(), and http.client.HTTPResponse: a correctly-returned non-blocking None gets mishandled one layer above io itself. wave's version joins tomllib and plistlib in the wrong-diagnosis crash flavor -- the error message blames an unrelated internal detail.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in wave.open(), reading a genuinely valid, complete WAV file through an io.BufferedReader over a custom non-blocking RawIOBase whose readinto() returns None once before returning the real WAV bytes. PlainHeaders is our extension; Python's wave and io modules are built in.