It seems to not be possible to typing.Unpack a union of objects such
that the following code snippet works:
from typing import Unpack, assert_type
def foo(*bar: Unpack[tuple[int,...]|tuple[str,...]]):
for baz in bar:
assert_type(baz, int|str)
...
I am aware that you can use typing.overload to define it like
from typing import Unpack, assert_type, overload
@overload
def foo(*bar: Unpack[tuple[int,...]]):
...
@overload
def foo(*bar: Unpack[tuple[str,...]]):
...
def foo(*bar: Unpack[tuple[int|str,...]]):
for baz in bar:
assert_type(baz, int|str)
...
but I specifically have a typing.TypeAlias which is a union of a
bunch of tuples, so overloading the function by listing out all the
possibilities is not only arduous but also defeats the purpose.
I tried to see if something like
from typing import Unpack, assert_type
def foo(*bar: Unpack[tuple[Unpack[tuple[int,...]]|Unpack[tuple[str,...]]]]):
for baz in bar:
assert_type(baz, int|str)
...
would work, which doesn’t report a type issue on the argument
declaration itself but the revealed type for baz ends up becoming *tuple[int,...]|*tuple[str,...] which seems bogus.
Is there a obvious (or not) solution that I am missing or is what I
am thinking of not possible as is (or otherwise)?
Yes, this is just an MRA, in my actual case (to my understanding) I can’t represent the type normally.
Also, I don’t believe that is an equivalent type to *(tuple[str,...]|tuple[int,...])?
That wasn’t an idea I had considered, but it appears to be identical to the original code:
from typing import Unpack, assert_type
def foo(*bar: Unpack[tuple[int,...]] | Unpack[tuple[str,...]]):
for baz in bar:
assert_type(baz, int|str) # Incorrect: `baz` is of type `*tuple[int,...] | *tuple[str,...]`
...
Technically it’s not quite the same, you’re correct, since the Union is applied to each element, rather than the whole tuple. But it fulfills the assert_type condition you used in your example. It wasn’t quite clear whether you cared about the element type or the type of the whole tuple. Going by your examples I assumed you only cared about the element type[1].
You could probably achieve what you wanted to achieve if you didn’t use varargs and just replaced it with a single tuple argument instead. I’m not surprised that neither a Union of Unpack nor an Unpack of a Union works correctly. It seems like a very fringe use-case.
This seems like something that would be easier to read as an overloaded function, especially since you can do it in two overloads with a default parameter. I don’t really see the point of trying to make this work with tuple unpacking, unless you’re trying to pass additional information into the function, so code flow analysis can correctly infer that unfiltered/filtered for the second parameter can only occur if the first parameter is True. But it doesn’t really seem worth obfuscating the function to the reader for that arguably very small benefit.
I also checked your Union of Unpack example in pyright and mypy playground. It seems like what you’re seeing is a pyright bug. mypy rejects both a Union of Unpack and an Unpack of a Union. Neither are really part of PEP-646, but only the latter is explicitly forbidden, that’s probably why pyright doesn’t detect the former as an invalid pattern but gives back a bogus type, because it’s not necessarily well-defined.
I see your point about obfuscation, and I am considering cleaning the code up to avoid having this issue, myself.
As for PEP-646 (assuming I correctly understood the section you are referring to), I interpreted that to only be with respect to TypeVarTuples; that is, «Instances of an (unpacked) TypeVarTuple cannot be “upgraded” (“widened”, “relaxed”, etc.)», rather than it being a statement about Unpack, Union, and Tuple in general. I think the use of Union there is an example and not a statement about typing.Union.
Explicitly forbidden may have been a poor choice of words on my part. I was talking about the fact that the PEP only allows Unpack to be used on either a TypeVarTuple or a tuple, so it follows that it is not allowed to be used with anything else, which includes Union[1].
Ah. You’re right. I’m trying to figure out what the semantics of Unpack accepting Union would be, but I am coming up short. I will let the issue rest for now, then. Thank you very much for all the help.
Unpacking Unions of tuple-types would be useful in other scenarios, for example when you want to annotate *args to support certain lengths of tuples. This could be useful when wrapping functions such as dict.get or dict.pop.