An unpacked tuple is available in a type hint as shown below:
# ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
v: tuple[int, int, *tuple[int, int, int]] = (0, 1, 2, 3, 4)
# No error
Now, when is an unpacked tuple really useful with a type hint?
An unpacked tuple is available in a type hint as shown below:
# ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
v: tuple[int, int, *tuple[int, int, int]] = (0, 1, 2, 3, 4)
# No error
Now, when is an unpacked tuple really useful with a type hint?
It isn’t useful per the typing’s documentation, so tools that adhere to the official specs will complain about it. But type hints can really be any objects as long as your toolings or libraries see meanings in them, so they can be useful as long as your own tools or libraries support them.
When doing something like x: tuple[str, int, *tuple[float, ...]. For a more specific example, this.
GeometricObject[Dimension: Literal[int]] = tuple[str, Dimension, *tuple[float, ...]]
My_1D: GeometricObject[Literal[1]] = ("P", Literal[1], 1.3)
My_2D: GeometricObject[Literal[2]] = ("Q", Literal[2], 9.4, 5.6)
...
I could imagine some more useful applications too, and I’ve also used it in other ways, but I don’t really recall any.
It’s also useful as the result of inference, to accurately represent a type. For instance if you had tup: tuple[int, ...] then did assert len(tup) >= 2, the type could be narrowed to tuple[int, int, *tuple[int, ...]], which would make the checker more accurate with later code analysis.
It can be used to annotate functions that take indeterminate amount of arguments, with a lower limit of at least <some number>.
asyncio.gather is one example - it could be called with any amount of awaitables, but it doesn’t make sense to call it with fewer than 2.
Prior to tuple unpacking, you could spell the signature of that function as
async def gather(c1: Awaitable, c2: Awaitable, /, *cn: Awaitable) -> ...
Note that if this is the actual implementation of that function, you now likely have to combine c1 and c2 into the same sequence as cn, before operating on the arguments.
With tuple unpacking, you can now spell the signature of that function as
async def gather(*coros: *tuple[Awaitable, Awaitable, *tuple[Awaitable, ...]]) -> ...
Now this function can directly operate on coros.
Ah, thanks. Never thought about the possibility of using the notation of tuple unpacking with an ellipsis to express a minimum of items. Might be worth including in the documentation.
One small thing I noticed:
Recently when I designed my library’s API I wanted to have a function that takes args like this.
I first went with the upper method, so like this:
def func(*args: *tuple[str, *tuple[str, ...]]) -> Any:
...
# Or like this
type TupleOfOneOrMore[T] = tuple[T, *tuple[T, ...]]
def func(*args: *TupleOfOneOrMore[str]) -> Any:
...
But I soon realized that using the older design patter, similar to how os.path.join does it, has multiple advantages:
def func(arg: str, /, *args: str) -> str:
...
Therefore i quickly switched over to this pattern.
The only negative I see about this is that it is harder (or impossible, depending on what you need) to make this signature work with a TypeVarTuple that should describe the signature of this function.
Not only that, it is also harder to read imo (probably because we all got used to the new pattern). Also naming the parameters is harder now, as you take away 2 names instead of one.