I am starting experiment with type annotations in Python 3 and have a problem with a function exclude_filter, specifically annotating the items in following code snippet (I am posting this unannotated). Briefly speaking I am trying to iterate over a list and filter out some items based on some criteria. And the type of the item in list is either instance of class or tuple of those instances in which case I am looking for criteria only in first member of the tuple.

@dataclass
class BaseItem:
    name: str
    something: int

def exclude_filter(items, matches):
    def item_matched(item, matches):
        name = item[0].name if isinstance(item, tuple) else item.name
        for match in matches:
            if match in name:
                return True
        return False    
    items[:] = [i for i in items if not item_matched(i, matches)]

FOOS = [BaseItem("1st foo", 10), BaseItem("2nd foo", 11)]
BARS = [BaseItem("1st bar", 20), BaseItem("2nd bar", 22)]
FOOS_AND_BARS = list(zip(FOOS, BARS))

exclude_filter(FOOS, ["1st"])
exclude_filter(BARS, ["2nd"])
exclude_filter(FOOS_AND_BARS, ["1st"])

print(FOOS)
# [BaseItem(name='2nd foo', something=11)]
print(BARS)
# [BaseItem(name='1st bar', something=20)]
print(FOOS_AND_BARS)
# [(BaseItem(name='2nd foo', something=11), BaseItem(name='2nd bar', something=22))]

I've tried obviously wrong items: List[BaseItem] with result:

Argument 1 to "exclude_filter" has incompatible type "List[Tuple[BaseItem, BaseItem]]"; expected "List[BaseItem]"

So I've tried item: List[Union[BaseItem, Tuple[BaseItem, BaseItem]]]:

Argument 1 to "exclude_filter" has incompatible type "List[BaseItem]"; expected "List[Union[BaseItem, Tuple[BaseItem, BaseItem]]]"

Then I've tried T = TypeVar("T", BaseItem, Tuple[BaseItem, BaseItem]) and items: List[T] a item: T but I got:

"Tuple[BaseItem, BaseItem]" has no attribute "name"

Well I tried even more obscure combinations but nothing seems to work. What is the correct way to annotate this code?

1

There are 1 best solutions below

0
On

Well I've found working but ugly solution to this problem and sadly it only works for homogenous lists.

BaseItemOrTuple = TypeVar(
    "BaseItemOrTuple",
    BaseItem,
    Tuple[BaseItem, BaseItem],
    Tuple[BaseItem, BaseItem, BaseItem],
    Tuple[BaseItem, BaseItem, BaseItem, BaseItem],
    # Et cetera for longer tuples
)

def exclude_filter(items: List[BaseItemOrTuple], matches: Sequence[str]) -> None:
    def item_matched(item: BaseItemOrTuple, matches: Sequence[str]) -> bool:
        if isinstance(item, tuple): #  cannot use one-liner here due to possible mypy bug
            name = item[0].name 
        else:
            name = item.name
        for match in matches:
            if match in name:
                return True
        return False    
    items[:] = [i for i in items if not item_matched(i, matches)]

The biggest problem with List being invariant is this:

Argument 1 to "exclude_filter" has incompatible type "List[Tuple[BaseItem, BaseItem]]"; expected "List[Tuple[BaseItem, ...]]"

That is the reason I cannot use Tuple[BaseItem, ....] in TypeVar and I must explicitely state all possible tuple lengths. It will be OK if I am using Sequence but due to items[:] operation I can't.

Also there is probably bug in mypy with conditional expression and using isinstance() so I need to use proper if-else block.