Skip to content

`hasattr(stream, "buffer") fails with ValueError if buffer is detached #160427

Description

@pekkaklarck

Bug 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

Activity

  1. added a commit that references this issue on Oct 23, 2026
  2. wlkr73 commented on Oct 23, 2026

    @wlkr73

    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.

  3. cmaloney commented on Oct 24, 2026

    @cmaloney
    Contributor

    cc: @vstinner gh-157364 backports made an API change. Revert or I can work next week on a new PR to fix?

  4. pekkaklarck commented on Oct 24, 2026

    @pekkaklarck
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions