Search code examples
pythonpython-typingmypy

mypy: "Item of Union has no attribute" error


Trying to learn to type hint in Python. Given these two functions:

from typing import Union, TextIO


def myfunc_ok(file: TextIO):
    mydump = file.read()
    print(mydump)


def myfunc_error(file: Union[str, TextIO]):
    mydump = file.read()
    print(mydump)

First one is ok to mypy, but it complains about the second one with an error

Item "str" of "Union[str, TextIO]" has no attribute "read"

Am I using type hinting incorrenctly in this case? (Using python3.7 with mypy 0.610, also tested with py3.6)


Solution

  • Your signature

    def myfunc_error(file: Union[str, TextIO]):
        ...
    

    says that file parameter can be str or TextIO, after that in function body you are trying to access .read attribute of file object, but in case of file being str there is no such attribute hence the error.

    You have at least 3 possibilities here:

    • do not support case with file being of type str and replace Union[str, TextIO] with TextIO
    • add explicit type checking using isinstance built-in in function body like

      import io
      ...
      def myfunc_error(file: Union[str, TextIO]):
          if isinstance(file, io.TextIOWrapper):
              mydump = file.read()
          else:
              # assuming ``file`` is a required object already
              mydump = file
          print(mydump)
      

      this may become difficult to maintain in the long term

    • write 2 different functions for given task: one for str parameter and one for TextIO parameter like

      def myfunc_error_str_version(file: str):
          mydump = file
          print(mydump)
      
      def myfunc_error_text_io_version(file: TextIO):
          mydump = file.read()
          print(mydump)
      

      this may cause a lot of naming problems (but it depends on the use-case)

    The last approach can be improved using functools.singledispatch decorator: in short this will allow us to define a generic function & use a name myfunc_error with overloads called based on the type of first positional argument (file in our case):

    import io
    from functools import singledispatch
    from typing import TextIO
    
    
    @singledispatch
    def myfunc_error(file: str):
        mydump = file
        print(mydump)
    
    # using ``typing.TextIO`` will not work because it's just an interface for type annotations,
    # "real" types are located at ``io`` module
    @myfunc_error.register(io.TextIOWrapper)
    def _(file: TextIO):
        mydump = file.read()
        print(mydump)
    

    Note: we can use any name we want instead of _ except myfunc_error, for the latter mypy will raise a name conflict error.