3

In the following code, I have a function that can take either a string, or some TextIOBase. If a string is passed, it is interpreted as a path to a file that should be opened and read. If a TextIOBase is passed, the contents of that stream will be read:

from typing import Union
import io

def function(foo: Union[str, io.TextIOBase]) -> None:
    if isinstance(foo, str):
        foo = open(foo, "w")

This seems like it should be okay, because Unions are supposed to be covariant, meaning that a subclass of one of the types in the union should satisfy the type annotation, and in this case the output type of open() is a subclass of TextIOBase. However, mypy complains with:

union.py:6: error: Incompatible types in assignment (expression has type "TextIO", variable has type "Union[str, TextIOBase]")
Found 1 error in 1 file (checked 1 source file)

I figured maybe there is an issue with the ambiguity of the return type of open(), based on the passed arguments, so I tried making a StringIO instead, but got the same error. Any thoughts? Why is mypy mad at me?

I have also tried this with some toy classes (e.g. Union[str, T1], then assigning a T2, where T2 is a subclass of T1), which mypy is perfectly happy with.

mkrieger1
  • 19,194
  • 5
  • 54
  • 65
youngmit
  • 800
  • 7
  • 17

1 Answers1

5

The typing module has a dedicated object for that: typing.TextIO. The return type of open is determined based on the mode argument and evaluates as one of these types: TextIO, BinaryIO.

a_guest
  • 34,165
  • 12
  • 64
  • 118
  • That did the trick. I was wondering what `TextIO` was from the mypy message. Still seems odd that the type checking doesn't work for the honest-to-goodness classes themselves though. Why is `TextIO` in `typing` needed? The documentation for `typing` doesn't provide much in the way of context. – youngmit Apr 02 '20 at 20:26
  • @youngmit I guess it's because the return type of `open` depends on its arguments, so you can't always determine what the return type is and need some kind of fallback (`typing.IO`). Imagine `open('test', mode=input())`; mypy will infer the type as `IO`. Hence, and in order to fulfill the subclass relationships with `TextIO` and `BinaryIO`, everything is defined in `typing`. – a_guest Apr 02 '20 at 20:47