2

I have a function that takes either a list of five ints or five ints as a tuple (*argv).

Here's my function heading: def __init__(self, *argv: Union[int, list]) -> None:

Later in this function I check the contents of this tuple to see if it's a list or five individual ints.

        if type(argv[0]) == int:
            self._l1 = [argv[0], argv[1], argv[2], argv[3], argv[4]]
        else:
            self._l1 = argv[0]

By this point in the code l1 is a list. self._l1 is definitely a list, it's no longer an int, it's a list.

However later in my code when I run this line:

        self._myvar = self._l1.count(1)

I am returned this error from MyPy

Incompatible types in assignment (expression has type "Union[int, List[Any]]", variable has type "List[Union[int, List[Any]]]")

Am I typing this wrong, what do I need to type this as? I've tried so many different types and keep getting errors.

As far as I can tell though my input is a tuple that will either contain a list of ints or five ints. I'd assume it's something like Union[Tuple[List[int]], Tuple[int, ...]] or just Union[List[int], int] or Union[List[int], Tuple[int, ...]], or something similar, but none of these are working for me.

  • I would *strongly* recommend changing your class so that `self._l1` is *always* a list, even if it only contains one item. Try to make one function do many different things just leads to unnecessary complexity. – chepner Dec 19 '21 at 23:47
  • Thanks, but self._l1 is always a list already, it's the argument to the function that can be either a tuple of ints or a tuple containing a single list. – DisplayName Dec 20 '21 at 00:05
  • Please show the line of input that leads to the error. Not clear whether the problem is when you pass 5 ints, or when you pass a tuple. The error message is a clue: note that it discusses `Union`. You want to make sure that `_li` becomes `List[int]`, **not** a type that still has `Union` in it. After those 4 lines of code that set `_li`, add a line that does `print(type(self._li))`. – ToolmakerSteve Dec 20 '21 at 00:38
  • That's still complicating your function more than necessary: always take a list, and let the caller be responsible for creating the list when necessary. – chepner Dec 20 '21 at 01:11

4 Answers4

1

Wasn't able to find why you version isn't working (as documentation states it should work for type(...) is syntax, but in my case changing type to if isinstance(argv[0], int): removed your mypy error.

kosciej16
  • 6,294
  • 1
  • 18
  • 29
  • Documentation is slightly unclear about this, but question is seeing expected behavior. Given that `a` is `X | Y`, `type(a) is X` will narrow `a` to `X` within the `if`. However, it will not narrow `a` to `Y` within the `else`. Documentation only claims the former, yet the question is trying to rely on the latter. `isinstance(a, X)` however will narrow both `a` to `X` within `if` and to `Y` within `else`. – Mario Ishac Dec 20 '21 at 01:09
  • As for why `type(a) is X` doesn't narrow `a` to `Y` in the `else`, it is because if `a` is `SubclassX` then the `else` will still be chosen, yet `issubclass(SubclassX, Y)` is `False` and accessing attributes of `Y` for `a` in the `else` would fail at runtime. – Mario Ishac Dec 20 '21 at 01:10
0

I recommend ensuring that _li is always type List[int].

I haven't tested the below code, but cast should work:

if isinstance(argv[0], int):
    self._l1 = [cast(int, argv[0]), cast(int, argv[1]), cast(int, argv[2]), cast(int, argv[3]), cast(int, argv[4])]
else:
    self._l1 = cast(List[int], argv[0])
print(type(self._li))

And where you declare _li and _myvar:

_li: List[int]
_myvar: List[int]
ToolmakerSteve
  • 18,547
  • 14
  • 94
  • 196
  • While this will satisfy `mypy`, it can fail at runtime since `argv[0]` may be an instance of a subclass of `int` and not `List[int]`. See [Extending Base Classes](https://stackoverflow.com/questions/33534/extending-base-classes-in-python). Would instead recommend going with other answer. – Mario Ishac Dec 20 '21 at 01:14
  • @MarioIshac - excellent point - I simply copied that from the question. I've edited my answer to use `isinstance`. I believe OP is best served by BOTH using `isinstance`, AND casting down to a simpler type. This proves the input is what it should be, and avoids type problems later on. (In the question, OP was attempting to make _myvar or _li be some complicated `Union` type.) – ToolmakerSteve Dec 20 '21 at 01:18
0

Why not use typing.overload?

It would look something like this:

from __future__ import annotations
from typing import overload

class MyClass:
    @overload
    def __init__(self: MyClass, a: int, b: int, c: int, d: int, e: int) -> None:
        ...

    @overload
    def __init__(self: MyClass, abcde: tuple[int,int,int,int,int]) -> None:
        ...

    def __init__(self, a, *bcde):
        if bcde:
            b, c, d, e = *bcde
            # etc
        else:
            a, b, c, d, e = *a
            # etc  
Broseph
  • 1,655
  • 1
  • 18
  • 38
0

So you defined argv as a sequence with the *, which could contain the int values, or a list of the int values. So what you really want to do is "flatten" the sequence.


def flatten(alist):
    """Flatten a nested set of lists into one list."""
    rv = []
    for val in alist:
        if isinstance(val, (list, tuple)):
            rv.extend(flatten(val))
        else:
            rv.append(val)
    return rv

class MyClass:
    def __init__(self, *argv: Union[int, list]):
        # argv is now a tuple of Union[int, list], or List[Union[int, List]], as the compiler says. 
        self._l1 = flatten(argv)
        # Now, you want to verify they are all ints, and that there are five.
        if len(self._l1) != 5:
            raise ValueError("Need five ints")
        if not all(isinstance(o, int) for o in self._l1):
            raise ValueError("not sequence of ints")
        # Now you have a single list of 5 ints.

Keith
  • 42,110
  • 11
  • 57
  • 76