Python mypy marks error when method parameter is type Union

654 views Asked by At

I have these python classes:

class LocalWritable(typing.TypedDict):
    file_name: str


class GSheetWritable(typing.TypedDict):
    tab_name: str


class S3Writable(typing.TypedDict):
    data_name: str
    table_name: str


WriterMeta = typing.Union[GSheetWritable, S3Writable, LocalWritable]

class DataWriter(ABC):
    """Defines the interface for all data writers"""

    @abstractmethod
    def write(self, data: pd.DataFrame, meta: WriterMeta, versionize: bool):
        """This method performs the writing of 'data'.

        Every class implementing this method must implement its writing
        using 'connector'
        """
        pass

class GSheetOutputWriter(DataWriter):
    def write(self, data: pd.DataFrame, meta: WriterMeta, versionize: bool):
        data = data.replace({np.nan: 0, np.Inf: "Inf"})

        print("Writing '{}' table to gsheet.".format(meta["tab_name"]))
        if self.new:
            tab = self.connector.get_worksheet(self.target.url, "Sheet1")
            self.connector.rename_worksheet(tab, meta["tab_name"])
            self.new = False
        else:
            tab = self.connector.add_worksheet(
                self.target, meta["tab_name"], rows=1, cols=1
            )

        time.sleep(random.randint(30, 60))
        self.connector.update_worksheet(
            tab, [data.columns.values.tolist()] + data.values.tolist()
        )

The problem is with the method write() when linting with python mypy, because it marks this error:

cost_reporter\outputs\__init__.py:209: error: TypedDict "S3Writable" has no key "tab_name"
cost_reporter\outputs\__init__.py:209: note: Did you mean "table_name" or "data_name"?
cost_reporter\outputs\__init__.py:209: error: TypedDict "LocalWritable" has no key "tab_name"

What I am trying to do is to implement three concrete classes based on the abstract class DataWriter, and each one shall implement its own write() method and each one shall receive one of the datatypes of WriterMeta union. The problem I am having is that python mypy validates the code against the three datatypes instead of any of them.

How can I do that?

EDIT

If I change the type of parameter meta to GsheetWritable(that is one of the three types of the union and the one expected by this concrete class), mypy marks this error:

cost_reporter\outputs\__init__.py:202: error: Argument 2 of "write" is incompatible with supertype "DataWriter"; supertype defines the argument type as "Union[GSheetWritable, S3Writable, LocalWritable]"
cost_reporter\outputs\__init__.py:202: note: This violates the Liskov substitution principle
1

There are 1 answers

0
Joshua Megnauth On BEST ANSWER

A Union works like unions in set theory. In other words, a Union consisting of multiple types is a type that supports only what's shared in common.

In order to use attributes (or whatever) of a specific type, you need to hint to mypy that you're constraining an instance. You can do this by casting the Union to a specific type, asserting that your object is whatever specific type, and others. The documentation lists ways to narrow types.

import typing
from abc import ABC, abstractmethod

class LocalWritable(typing.TypedDict):
    file_name: str


class GSheetWritable(typing.TypedDict):
    tab_name: str


class S3Writable(typing.TypedDict):
    data_name: str
    table_name: str


WriterMeta = typing.Union[GSheetWritable, S3Writable, LocalWritable]


class DataWriter(ABC):
    @abstractmethod
    def write(self, data: str, meta: WriterMeta):
        pass


class GSheetOutputWriter(DataWriter):
    def write(self, data: str, meta: WriterMeta):
        # LOOK HERE! The cast hints to mypy that meta is a GSheetWritable.
        meta_cast: GSheetWritable = typing.cast(GSheetWritable, meta)
        print("Writing '{}' table to gsheet.".format(meta_cast["tab_name"]))

Further reading