GeezerVison is showing github.com — `hasattr(stream, "buffer")` fails with ValueError if buffer is detached · Issue #160427 · python/cpython
hasattr(stream, "buffer") fails with ValueError if buffer is detached #160427
DescriptionBug report
>>> stream = open("example.txt", "w")
>>> hasattr(stream, "buffer")
True
>>> stream.detach()
<_io.BufferedWriter name='example.txt'>
>>> hasattr(stream, "buffer")
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
hasattr(stream, "buffer")
~~~~~~~^^^^^^^^^^^^^^^^^^
ValueError: underlying buffer has been detached
This occurs with Python 3.14.9 and 3.15.1, but not with earlier versions such as 3.14.8 and 3.15.0. Tested both on Linux and on Windows. I consider hasattr failing with an exception pretty bad, especially when using it with standard library objects, and the behavior changing in a bug fix release makes it even worse.
I noticed this when a unit test for our custom isatty utility started to fail (robotframework/robotframework#5825). We have that utility so that we can safely check is a stream connected to console or not. It used to look like this:
def isatty(stream):
# first check if buffer was detached
if hasattr(stream, "buffer") and stream.buffer is None:
return False
if not hasattr(stream, "isatty"):
return False
try:
return stream.isatty()
except ValueError: # Occurs if file is closed.
return False
I already fixed this in our end by simply removing the separate detached buffer check. stream.isatty() raised a ValueError if its buffer is detached, similarly when the stream is closed, and we already handle that. Apparently the separate check was added before handling closed streams and nobody noticed earlier that the check is redundant.
CPython versions tested on:
3.14, 3.15
Operating systems tested on:
Linux, Windows
Linked PRs
Activityscrieve commented on Oct 23, 2026 I would like to settle the intended behaviour here before asking anyone to review a patch.
The case for None:
- In 3.14.8
buffer was a member descriptor (_Py_T_OBJECT), so a detached wrapper read as None and hasattr() did not raise.
_pyio.TextIOWrapper.buffer still returns None after detach, so the two implementations disagree today.
- Nothing in the
io docs says the attribute raises. The nearest statement is that the TextIOBase is unusable after detach, which reads as being about operations.
The case for the current ValueError is that gh-157364 deliberately routed the attribute through buffer_access_safe() while closing a use-after-free. That guard is still needed for reads and writes; the open question is only whether the attribute getter is the right place to raise it.
Proposal: the getter returns None when the wrapper is detached but initialised, and keeps raising ValueError for the uninitialised case. Every other caller keeps the existing guard, so nothing that gh-157364 protected changes.
If a triager or developer would rather keep the ValueError, that is a fine answer and I will drop the patch. A patch against this proposal is at #160467, held as a draft until there is agreement here. cmaloney commented on Oct 24, 2026 cc: @vstinner gh-157364 backports made an API change. Revert or I can work next week on a new PR to fix? pekkaklarck commented on Oct 24, 2026 FWIW, as a user I'd expect hasattr (and isinstance and other such functions) to return a Boolean and never raise. I know this depends on the underlying type, but I'd expect this to hold at least with the standard library types. changed the title [-]`hasattr(stream, "buffer") fails with ValueError if buffer is detached[/-] [+]`hasattr(stream, "buffer")` fails with ValueError if buffer is detached[/+] on Oct 28, 2026cmaloney commented on Oct 28, 2026 I did an audit and there are quite a few attributes that raise after a change, such as memoryview attributes after .release(). I suspect moving to fully uniform behavior is out of question. That said, this definitely shouldn't have changed in a point release so working to restore the old behavior. cmaloney commented on Oct 28, 2026 This actually broke in a free-threading change: gh-154523. I'm leaning always return None here where self->buffer is NULL which should backport easily. I think the None behavior from the descriptor is good to restore.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|