Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)

like image 537
Stagrovin Avatar asked Jan 28 '23 15:01

Stagrovin


1 Answers

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.

like image 157
Azat Ibrakov Avatar answered Jan 31 '23 23:01

Azat Ibrakov