8

When writing a class that implements a file-like interface we can inherit one of the abstract base classes from the io module, for example TextIOBase, as shown in Adapt an iterator to behave like a file-like object in Python.

On the other hand, in type annotations we are supposed to use the classes derived from typing.IO (e.g. TextIO) to represent such objects, as shown in Type hint for a file or file-like object? or Type-checking issue with io.TextIOBase in a Union.

However, this does not seem to work as I expected:

import io
import sys
import typing

class MyIO(io.TextIOBase):
    def write(self, text: str):
        pass

def hello(f: typing.TextIO):
    f.write('hello')

hello(sys.stdout)             # type checks
hello(open('temp.txt', 'w'))  # type checks
hello(MyIO())                 # does not type check

When running mypy on this code (using Python 3.7.3 and mypy 0.910), we get

error: Argument 1 to "hello" has incompatible type "MyIO"; expected "TextIO"

Question

How can the MyIO class be written such that it is accepted as a function argument of type typing.TextIO (without just using typing.cast(typing.TextIO, ...))?

Failed attempts

  1. Using typing.TextIO as a base class is not possible:

    When using class MyIO(typing.TextIO):

    error: Cannot instantiate abstract class "MyIO" with abstract attributes "__enter__", "__exit__", ... and "writelines" (15 methods suppressed)

    When using class MyIO(io.TextIOBase, typing.TextIO)::

    error: Definition of "readlines" in base class "IOBase" is incompatible with definition in base class "IO"

    and the same for several other methods.

  2. Overriding __new__ and annotating typing.TextIO as return type does not work:

    def __new__(cls, *args, **kwargs) -> typing.TextIO:                        
        return super().__new__(cls, *args, **kwargs)
    

    results in

    error: Incompatible return type for "__new__" (returns "TextIO", but must return a subtype of "MyIO")
    error: Incompatible return value type (got "MyIO", expected "TextIO")

Or is this already supposed to work, and I'm using too old a version of Python and/or mypy? Using --python-version 3.8 or 3.9 or 3.10 as option for mypy does not change anything, however.

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
mkrieger1
  • 19,194
  • 5
  • 54
  • 65
  • 1
    I'm using Python 3.9 and pyright. [io.pyi](https://github.com/python/typeshed/blob/master/stdlib/io.pyi#L133) in the typesheds has `TextIOWrapper` inherit from `typing.TextIO`, but `TextIOBase` does not do this. `reveal_type(open('temp.txt', 'w'))` shows `TextIOWrapper` for me. Inheriting `class MyIO(io.TextIOBase, typing.TextIO)` works for me, and pyright does not complain. Surely `TextIOBase` should inherit from `typing.TextIO` then in the typesheds. – eugenhu Oct 14 '21 at 12:19
  • It looks like MyPy (0.910) has a problem wit the weird way that the ``io`` methods are overridden in typeshed. Since ``TextIOBase`` inherits from ``IOBase``, MyPy e.g. expects *both* ``__iter__: () -> Iterator[str]`` and ``__iter__: () -> Iterator[bytes]``. – MisterMiyagi Oct 14 '21 at 12:29
  • Interesting upstream issue: [python/typeshed#6061](https://github.com/python/typeshed/issues/6061) – Jonathon Reinhart Mar 13 '23 at 20:04

2 Answers2

1

Use io.StringIO instead

import io
import sys
import typing


class MyIO(io.StringIO):
    def write(self, text: str):
        pass


def hello(f: typing.TextIO):
    f.write("hello")


hello(sys.stdout)             # type checks
hello(open("temp.txt", "w"))  # type checks
hello(MyIO())                 # type checks
 
Mohammad Jafari
  • 1,742
  • 13
  • 17
  • 2
    [`io.StringIO`](https://docs.python.org/3/library/io.html#io.StringIO) is a concrete implementation of an in-memory text file; I don't think it is at all a good choice for a base type of an arbitrary file-like implementation. – Jonathon Reinhart Mar 13 '23 at 20:05
1

I think your first attempt is actually correct, it just requires you to implement all the abstract methods (as the error says). You dont have to put actual logic there. The following class would do the trick:

import io
from types import TracebackType
from typing import Optional, Type, Iterator, AnyStr, Iterable, TextIO

import sys
import typing

class MyIO(typing.TextIO):
    def __enter__(self) -> TextIO:
        pass

    def close(self) -> None:
        pass

    def fileno(self) -> int:
        pass

    def flush(self) -> None:
        pass

    def isatty(self) -> bool:
        pass

    def read(self, n: int = ...) -> AnyStr:
        pass

    def readable(self) -> bool:
        pass

    def readline(self, limit: int = ...) -> AnyStr:
        pass

    def readlines(self, hint: int = ...) -> typing.List[AnyStr]:
        pass

    def seek(self, offset: int, whence: int = ...) -> int:
        pass

    def seekable(self) -> bool:
        pass

    def tell(self) -> int:
        pass

    def truncate(self, size: Optional[int] = ...) -> int:
        pass

    def writable(self) -> bool:
        pass

    def writelines(self, lines: Iterable[AnyStr]) -> None:
        pass

    def __next__(self) -> AnyStr:
        pass

    def __iter__(self) -> Iterator[AnyStr]:
        pass

    def __exit__(self, t: Optional[Type[BaseException]], value: Optional[BaseException],
                 traceback: Optional[TracebackType]) -> Optional[bool]:
        pass

    def write(self, text: str):
        pass

def hello(f: typing.TextIO):
    f.write('hello')

hello(sys.stdout)             # type checks
hello(open('temp.txt', 'w'))  # type checks
hello(MyIO())                 # does not type check
leberknecht
  • 1,526
  • 15
  • 27
  • I think it would be a much better idea to raise [`io.UnsupportedOperation`](https://docs.python.org/3/library/io.html#io.UnsupportedOperation) rather than simply `pass`ing in the unimplemented methods. From [the docs](https://docs.python.org/3/library/io.html#class-hierarchy): *"implementations may raise a `ValueError` (or `UnsupportedOperation`) when operations they do not support are called."* – Jonathon Reinhart Mar 13 '23 at 20:08