Skip to content

TypeError should be raised for tuple unpacking size mismatch instead of ValueError #138236

@peturingi

Description

@peturingi

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 $T$, the n-tuple type is formally defined as: $$\text{n-tuple}[T] = \underbrace{T \times T \times \ldots \times T}_{n \text{ times}}$$
where the cardinality of the product type is $$|\text{n-tuple}[T]| = |T|^n$$

For distinct values of $n$ and $m$ where $n \neq m$, we have $$\text{n-tuple}[T] \cap \text{m-tuple}[T] = \emptyset$$

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:

$$\frac{v_1 : T \quad v_2 : T \quad v_3 : T}{(v_1, v_2, v_3) : T^3}$$

$$\frac{v_1 : T \quad v_2 : T \quad v_3 : T \quad v_4 : T}{(v_1, v_2, v_3, v_4) : T^4}$$

There is no derivation rule which allow for $$\nvdash (v_1, v_2, v_3) : T^4$$

This is to say that the value $(v_1, v_2, v_3)$ of type $T^3$ cannot inhabit type $T^4$, not because the value is wrong, but because tuples of different arities are fundamentally different types.

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 $$T^4 &lt;: T^3$$

while explicitly disallowing the reverse:

$$T^3 \not<: T^4$$

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

$$ E \Downarrow (v_1,\dots,v_n);\Rightarrow; \begin{cases} (\text{let } x_1,\dots,x_m = E \text{ in } E') \Downarrow E'[v_1/x_1,\dots,v_m/x_m], & \text{if } n = m,\\ \text{raise Error}, & \text{if } n \neq m. \end{cases} $$

Extended Iterable Unpacking (PEP 3132)

$$ \frac{E \Downarrow (v_1,\dots,v_n) \quad n \ge m-1} {(\text{let } x_1,\dots,x_{m-1},,\ast x_m = E \text{ in } E') \Downarrow E'[v_1/x_1,\dots,v_{m-1}/x_{m-1},(v_m,\dots,v_n)/x_m]} $$

Under Fixed-Length Unpacking rule, failure of the side condition $n \neq m$ was is interpreted in CPython as a 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions