-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
Description
Bug report
Bug description:
When unpacking a tuple into a different number of variables than the tuple contains, Python raises ValueError
. This violates fundamental type-theoretic principles and contradicts Python's own error hierarchy documentation.
Current Behaviour
def process_quadruple(data: tuple[int, int, int, int]):
a, b, c, d = data # Raises ValueError if len(data) != 4.
return a + b + c + d
process_quadruple((1, 2, 3)) # "ValueError: not enough values to unpack (expected 4, got 3)".
Expected Behaviour
process_quadruple((1, 2, 3))
should raise TypeError
, not ValueError
.
Arguments
Type-Theory
In type theory, a tuple is a product type. For a type
where the cardinality of the product type is
For distinct values of
which makes it evident that the types of a 3-tuple and a 4-tuple are disjoint.
To further support this argument, we can formally define the typing judgement for tuple membership as:
There is no derivation rule which allow for
This is to say that the value
Subtyping Relations of Tuples in Python
In structural type systems, tuple types have restricted subtyping relations.
That is also the case in Python:
# Python supports T⁴ <: T³ using explicit type narrowing via extended iterable unpacking:
a, b, c, *_ = (1, 2, 3, 4) # Valid: 4-tuple satisfies 3-element unpacking.
This demonstrates that Python recognises the width subtyping relation to the point
that it allows a 4-tuple to be unpacked into 3 variables by ignoring the extra elements, thereby satisfying the subtyping relation
while explicitly disallowing the reverse:
a, b, c, d = (1, 2, 3) # Invalid: 3-tuple cannot satisfy 4-element unpacking.
Type Annotations
Python's own annotation syntax allows for explicit distinction between tuples of different arities:
from typing import get_type_hints
def f(x: tuple[int, int, int]): pass
def g(x: tuple[int, int, int, int]): pass
print(get_type_hints(f)['x'] == get_type_hints(g)['x']) # False
Static type checkers like mypy correctly identify this as a type error:
def process(x: tuple[int, int, int, int]):
return sum(x)
process((1, 2, 3)) # mypy: Argument 1 has incompatible type "tuple[int, int, int]"
# expected "tuple[int, int, int, int]"
Formal Semantics of the Error
We can characterise the two assignment-unpacking operational rules (ignoring starred-assignment in literals) as:
Fixed-Length Unpacking
Extended Iterable Unpacking (PEP 3132)
Under Fixed-Length Unpacking rule, failure of the side condition ValueError
.
However, once extended iterable unpacking was introduced, this justification becomes less coherent: tuples of differing arities can be valid inputs under the language semantics.
The error in the fixed-length case is therefore not due to a "malformed value", but to a mismatch between the tuple's arity and the expected type of the left-hand side, which makes it more naturally classified as a TypeError
.
Comparison with Other Languages
Haskell
-- Type error at compile time
let (a, b, c, d) = (1, 2, 3) -- Couldn't match expected type '(a, b, c, d)'
-- with actual type '(Integer, Integer, Integer)'
Rust
let (a, b, c, d) = (1, 2, 3); // Error: mismatched types
// expected tuple of 4 elements, found tuple of 3 elements
OCaml
let (a, b, c, d) = (1, 2, 3);; (* Error: This expression has type int * int * int
but an expression was expected of type 'a * 'b * 'c * 'd *)
All these languages correctly identify this as a type error.
Proposed Fix
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1082,14 +1082,14 @@ _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v,
w = PyIter_Next(it);
if (w == NULL) {
/* Iterator done, via error or exhaustion. */
if (!_PyErr_Occurred(tstate)) {
if (argcntafter == -1) {
- _PyErr_Format(tstate, PyExc_ValueError,
+ _PyErr_Format(tstate, PyExc_TypeError, // +
"not enough values to unpack "
"(expected %d, got %d)",
argcnt, i);
}
else {
_PyErr_Format(tstate, PyExc_ValueError,
"not enough values to unpack "
"(expected at least %d, got %d)",
argcnt + argcntafter, i);
}
}
goto Error;
}
@@ -1110,20 +1110,20 @@ _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v,
if (w == NULL) {
if (_PyErr_Occurred(tstate))
goto Error;
Py_DECREF(it);
return 1;
}
Py_DECREF(w);
if (PyList_CheckExact(v) || PyTuple_CheckExact(v)
|| PyDict_CheckExact(v)) {
ll = PyDict_CheckExact(v) ? PyDict_Size(v) : Py_SIZE(v);
if (ll > argcnt) {
- _PyErr_Format(tstate, PyExc_ValueError,
+ _PyErr_Format(tstate, PyExc_TypeError, // +
"too many values to unpack (expected %d, got %zd)",
argcnt, ll);
goto Error;
}
}
- _PyErr_Format(tstate, PyExc_ValueError,
+ _PyErr_Format(tstate, PyExc_TypeError, // +
"too many values to unpack (expected %d)",
argcnt);
goto Error;
}
@@ -1137,7 +1137,7 @@ _PyEval_UnpackIterableStackRef(PyThreadState *tstate, PyObject *v,
ll = PyList_GET_SIZE(l);
if (ll < argcntafter) {
- _PyErr_Format(tstate, PyExc_ValueError,
+ _PyErr_Format(tstate, PyExc_ValueError, // (unchanged – extended unpacking remains ValueError)
"not enough values to unpack (expected at least %d, got %zd)",
argcnt + argcntafter, argcnt + ll);
goto Error;
}
CPython versions tested on:
3.13
Operating systems tested on:
macOS