What is TextIO?

This piece of code

import io
import sys

def func()->io.TextIOBase:
    return sys.stdout

print(isinstance(func(), io.TextIOBase))

prints True, but mypy doesn’t like it:

textiotest.py:5: error: Incompatible return value type (got "TextIO", expected "TextIOBase")  [return-value]
Found 1 error in 1 file (checked 1 source file)

I can’t figure out what TextIO is. The io docs does not mention it.

Maybe typing.TextIO

2 Likes

That looks right, thanks!

But why is io.TextIOBase not the right return type? Is it platform dependent?

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)

3 Likes

So this is a bug?

This code

import sys
from typing import TextIO

def func()->TextIO:
    return sys.stdout

print(isinstance(func(), TextIO))

satisfies mypy, but prints False

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.

It does match the API shape. It would make sense to me that typing.TextIO (and typing.IO, typing.BinaryIO) should be added as “virtual” abcs abc — Abstract Base Classes — Python 3.12.4 documentation

The reason why mypy thinks the type is TextIO is because of the type given to sys.stdout in the typeshed stubs here: typeshed/stdlib/sys/__init__.pyi at 9817430896540ba2a566bd02e51ac72d79ef47ae · python/typeshed · GitHub. Mypy uses typeshed’s stubs as the single source of truth for the standard library.

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(...)
3 Likes

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.

4 Likes

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 :slight_smile:

1 Like