# A tuple actual maps to a fixed number of formals.
for_inrange(len(actualt.items)):
iffi<nformals:
ifformal_kinds[fi] !=nodes.ARG_STAR2:
formal_to_actual[fi].append(ai)
else:
break
ifformal_kinds[fi] !=nodes.ARG_STAR:
fi+=1
For input x1, this just sees tuple[str, *tuple[str, ...]] and then maps str -> a1 and *tuple[str, ...] -> a2, and then assumes argument for a3 is missing.
This is a bug, and IMO all those calls should be accepted. We accept unpacked variadic tuples everywhere when they can possibly be expanded into a correct sequence - this aims at maintaining the balance between correctness and usability.
@sterliakov So, If we treat tuple[T, ...] as AnyOf rather than Union, in this extended example the only thing that should fail is takes_exactly_3(*x4), right?
I have thought about this some more, and I am starting to believe this is generally not feasible due to combinatorial explosion, especially when the variable tuples have different types.
But I came up with a general approach that limits the computational complexity and is sufficiently close to what mypy is already doing, so that I believe it could be integrated.
Setup
Assume we are only dealing with positional arguments
Assume that all positional arguments are passed in the form of *args1
So w.l.o.g. our function signature is f(*x1, *x2, ...., *xn)
Define the Tuple Normal Form (TNF):
Given any tuple tuple[T1, ..., Tn] or tuple[T, ...] define the TNDF as the unique tuple tuple[P1, ..., Pn, *Vs?, S1, ..., Sm] that splits the tuple into 3 parts: a maximal fixed size finite prefix P1-Pn, a potentially non-existent variable part *Vs?, and a maximal finite size suffix S1-Sm (which is always empty if there is no variable part)
Convert each argument x to TNF. Let c denote the first argument with a variable length part. So w.l.o.g. out call signature is:
f(*a1, ..., *an, *c, *b1, ..., *bm)
where each a is a statically known fixed length tuple, and c is a variable length tuple.
Algorithm
Perform a split at c:
Let tuple[P1, ..., Pn, *Vs, S1, ..., Sm] be the TNF of c.
Map the prefix to the remaining formal arguments as usual. Then there are 3 options:
We exhausted the formal positional args before reaching *Vs -> we know there are too many arguments!
We exhaust the prefix before reaching an ARG_STAR formal -> tail_slice=[*Vs, S1, ..., Sm].
We reached an ARG_STAR formal before exhausting the prefix -> tail_slice=[Pk, ..., Pn, *Vs, S1, ..., Sm].
Recombine the tail slice with the remaining arguments into a single *args, and express it in TNF:
*[Pk, ..., Pn, *Vs, S1, ..., Sm], *b1, ...., *bm ⟶ [P1', ..., Pr', *Vs', S1', ..., St']
call this the tail_arg. So we have reinterpreted our original call signature as:
f(p1, p2, ..., pn, *tail_args)
where each p is a regular (non star) positional argument, and tail_args is tuple[P1', ..., Pr', *Vs', S1', ..., St']
by method of statically extracting the maximal fixed length prefix.
Map the tail_args onto the remaining formal positional arguments.
Type check the prefix part as usual element-wise.
To type check the tail, we know *tail_args maps to some formal arguments with kinds: (ARG_POS, ...., ARG_POS, ARG_STAR) with types T1, ..., Tn and some unpacked tuple *Ts for ARG_STAR. (ignore paramspec for now)
combine all these types into a tuple tuple[T1, ..., Tn, *Ts]
express this in TNF.
compare the result with tail_args
Finally, regarding the TNF, one needs to see how multiple variable length parts need to be combines. For example:
Then the TNF is tuple[int, *VS, str], where Vs is one of:
tuple[Any, ...] (fallback)
tuple[object, ...] (join)
tuple[int | None | str] (union)
tuple[*tuple[int, ...], None, *tuple[str, ...]] (disallowed by spec - only one variable length part allowed).
Application to the original example
Considering the example #19692 (comment), this should give us the desired behavior, since all the tuple argument types are str, the recombination into TNF of the tail does not produce any unexpected types.
Footnotes
We can always interpret a regular positional argument as a *args by simply wrapping it in a tuple: f(x) ⟺ f(*(x,)) ↩
the only thing that should fail is takes_exactly_3(*x4), right?
Yes, I think so - we really don't want to restrict unpacking variadic tuples into fixed arguments, this would break too may programs in wild (though maybe it can be made configurable via a separate flag, --strict-tuple-unpacking?)
Activity
randolf-scholz commented on Aug 19, 2025
This is causes by bad formal_to_actual mapping:
mypy/mypy/argmap.py
Lines 57 to 66 in 0d23c61
For input x1, this just sees tuple[str, *tuple[str, ...]] and then maps str -> a1 and *tuple[str, ...] -> a2, and then assumes argument for a3 is missing.
sterliakov commented on Aug 20, 2025
This is a bug, and IMO all those calls should be accepted. We accept unpacked variadic tuples everywhere when they can possibly be expanded into a correct sequence - this aims at maintaining the balance between correctness and usability.
randolf-scholz commented on Aug 20, 2025
A second example: https://mypy-play.net/?mypy=latest&python=3.12&gist=b13968d8aa8c885711e80ff40abf5074
here, two equivalent types lead to different inference.
randolf-scholz commented on Aug 21, 2025
And here's if we also check a function that takes exactly 3 arguments using the original setup:
https://mypy-play.net/?mypy=1.17.1&python=3.12&gist=4837507130a3fba367e8458dde74851d
@sterliakov So, If we treat tuple[T, ...] as AnyOf rather than Union, in this extended example the only thing that should fail is takes_exactly_3(*x4), right?
randolf-scholz commented on Aug 21, 2025
I have thought about this some more, and I am starting to believe this is generally not feasible due to combinatorial explosion, especially when the variable tuples have different types.
But I came up with a general approach that limits the computational complexity and is sufficiently close to what mypy is already doing, so that I believe it could be integrated.
Setup
Assume we are only dealing with positional arguments
Assume that all positional arguments are passed in the form of *args1
So w.l.o.g. our function signature is f(*x1, *x2, ...., *xn)
Define the Tuple Normal Form (TNF):
Given any tuple tuple[T1, ..., Tn] or tuple[T, ...] define the TNDF as the unique tuple tuple[P1, ..., Pn, *Vs?, S1, ..., Sm] that splits the tuple into 3 parts: a maximal fixed size finite prefix P1-Pn, a potentially non-existent variable part *Vs?, and a maximal finite size suffix S1-Sm (which is always empty if there is no variable part)
Convert each argument x to TNF. Let c denote the first argument with a variable length part. So w.l.o.g. out call signature is:
f(*a1, ..., *an, *c, *b1, ..., *bm)
where each a is a statically known fixed length tuple, and c is a variable length tuple.
Algorithm
Perform a split at c:
Recombine the tail slice with the remaining arguments into a single *args, and express it in TNF:
*[Pk, ..., Pn, *Vs, S1, ..., Sm], *b1, ...., *bm ⟶ [P1', ..., Pr', *Vs', S1', ..., St']
call this the tail_arg. So we have reinterpreted our original call signature as:
f(p1, p2, ..., pn, *tail_args)
where each p is a regular (non star) positional argument, and tail_args is tuple[P1', ..., Pr', *Vs', S1', ..., St']
by method of statically extracting the maximal fixed length prefix.
Map the tail_args onto the remaining formal positional arguments.
Type check the prefix part as usual element-wise.
To type check the tail, we know *tail_args maps to some formal arguments with kinds: (ARG_POS, ...., ARG_POS, ARG_STAR) with types T1, ..., Tn and some unpacked tuple *Ts for ARG_STAR. (ignore paramspec for now)
Finally, regarding the TNF, one needs to see how multiple variable length parts need to be combines. For example:
tuple[int, *tuple[int, ...], None, *tuple[str, ...], str]
Then the TNF is tuple[int, *VS, str], where Vs is one of:
Application to the original example
Considering the example #19692 (comment), this should give us the desired behavior, since all the tuple argument types are str, the recombination into TNF of the tail does not produce any unexpected types.
Footnotes
We can always interpret a regular positional argument as a *args by simply wrapping it in a tuple: f(x) ⟺ f(*(x,)) ↩
sterliakov commented on Aug 30, 2025
Yes, I think so - we really don't want to restrict unpacking variadic tuples into fixed arguments, this would break too may programs in wild (though maybe it can be made configurable via a separate flag, --strict-tuple-unpacking?)