The typing stubs don’t reflect the runtime behavior here. I suspect that typing.TextIO should be a corresponded/alias/(maybe protocol?) of io.TextIOBase, but it isn’t. You are completely correct that io is the correct return type, isinstance(open(..., mode='rt'), io.TextIOBase) is True after all. (and is false for typing.TextIO)
I’ve been playing with / exploring open() recently, and found this comes from when PEP 3116 – New I/O | peps.python.org was implemented. sys.stdout is a file object created during startup by a call to open(). The open() call inspects its arguments to construct any of a number of IO classes to support specific usecases (Binary, Text, Buffered, Read, Write, ReadWrite, …).
re: bug, the behavior part of this is isinstance() is looking for "is this an object this class or derived from it (Built-in Functions — Python 3.12.4 documentation). TextIOBase doesn’t inherit from TextIO, so it isn’t a subclass.
The type of sys.stdout is a common source of complaints we receive at typeshed, but changing it to a more precise type would unfortunately not be safe in this instance. In lots of situations, users have found that sys.stdout is not an instance of io.TextIOBase, as some library they are using has patched sys.stdout to be something that’s duck-type compatible but not an instance of io.TextIOBase. Quoting the comment in the stub:
# TextIO is used instead of more specific types for the standard streams,
# since they are often monkeypatched at runtime. At startup, the objects
# are initialized to instances of TextIOWrapper, but can also be None under
# some circumstances.
#
# To use methods from TextIOWrapper, use an isinstance check to ensure that
# the streams have not been overridden:
#
# if isinstance(sys.stdout, io.TextIOWrapper):
# sys.stdout.reconfigure(...)
We could probably fix that at runtime so that TextIO recognises more things from the io module as virtual subclasses. I’d consider that feature request.
Asking for it to be correct is not necessarily asking for it to be more precise.
It could be made more correct by making it more general.
If we want to support the monkeypatching that people are doing, it seems to me it would be better to change the typeshed type to a protocol that includes both what sys.stdout actually is and what people monkeypatch it to.
This current workaround is not safe. I think it should be considered a bug in typeshed.
Making the virtual subclassing with TextIO is ok, but that should not be required in order to have something in typeshed that is both correct and supports the monkeypatching case.
I sought to explain the reasons for the current behaviour, not to claim that typeshed was perfect! Feel free to file bugs on our issue tracker, or – better yet – to file a PR improving the situation