]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145876: Do not mask AttributeErrors raised during dictionary unpacking (GH-145906)
authorSerhiy Storchaka <storchaka@gmail.com>
Thu, 26 Mar 2026 13:48:57 +0000 (15:48 +0200)
committerGitHub <noreply@github.com>
Thu, 26 Mar 2026 13:48:57 +0000 (13:48 +0000)
AttributeErrors raised in keys() or __getitem__() during
dictionary unpacking ({**mymapping} or func(**mymapping)) are
no longer masked by TypeError.

Lib/test/test_extcall.py
Lib/test/test_unpack_ex.py
Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst [new file with mode: 0644]
Modules/_testinternalcapi/test_cases.c.h
Python/bytecodes.c
Python/ceval.c
Python/executor_cases.c.h
Python/generated_cases.c.h

index f003a5837ae0eb23a072336ccca6a81b147e1b15..20dd16dd05d12d30fe8dd2d0296ec346664c120f 100644 (file)
@@ -329,6 +329,22 @@ What about willful misconduct?
       ...
     TypeError: Value after ** must be a mapping, not function
 
+    >>> class OnlyKeys:
+    ...     def keys(self):
+    ...         return ['key']
+    >>> h(**OnlyKeys())
+    Traceback (most recent call last):
+      ...
+    TypeError: 'OnlyKeys' object is not subscriptable
+
+    >>> class BrokenKeys:
+    ...     def keys(self):
+    ...         return 1
+    >>> h(**BrokenKeys())
+    Traceback (most recent call last):
+      ...
+    TypeError: test.test_extcall.BrokenKeys.keys() must return an iterable, not int
+
     >>> dir(b=1, **{'b': 1})
     Traceback (most recent call last):
       ...
@@ -540,6 +556,151 @@ Same with keyword only args:
 
 """
 
+def test_errors_in_iter():
+    """
+    >>> class A:
+    ...     def __iter__(self):
+    ...         raise exc
+    ...
+    >>> def f(*args, **kwargs): pass
+    >>> exc = ZeroDivisionError('some error')
+    >>> f(*A())
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> f(*A())
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> f(*A())
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_next():
+    """
+    >>> class I:
+    ...     def __iter__(self):
+    ...         return self
+    ...     def __next__(self):
+    ...         raise exc
+    ...
+    >>> class A:
+    ...     def __iter__(self):
+    ...         return I()
+    ...
+
+    >>> def f(*args, **kwargs): pass
+    >>> exc = ZeroDivisionError('some error')
+    >>> f(*A())
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> f(*A())
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> f(*A())
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_keys():
+    """
+    >>> class D:
+    ...     def keys(self):
+    ...         raise exc
+    ...
+    >>> def f(*args, **kwargs): pass
+    >>> exc = ZeroDivisionError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_keys_next():
+    """
+    >>> class I:
+    ...     def __iter__(self):
+    ...         return self
+    ...     def __next__(self):
+    ...         raise exc
+    ...
+    >>> class D:
+    ...     def keys(self):
+    ...         return I()
+    ...
+    >>> def f(*args, **kwargs): pass
+    >>> exc = ZeroDivisionError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_getitem():
+    """
+    >>> class D:
+    ...     def keys(self):
+    ...         return ['key']
+    ...     def __getitem__(self, key):
+    ...         raise exc
+    ...
+    >>> def f(*args, **kwargs): pass
+    >>> exc = ZeroDivisionError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> f(**D())
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
 import doctest
 import unittest
 
index 904cf4f626ae78450428a683524128587d361094..d3ba8133c41d5716061fecda7ab6c275b2568a69 100644 (file)
@@ -134,6 +134,22 @@ Dict display element unpacking
     ...
     TypeError: 'list' object is not a mapping
 
+    >>> class OnlyKeys:
+    ...     def keys(self):
+    ...         return ['key']
+    >>> {**OnlyKeys()}
+    Traceback (most recent call last):
+      ...
+    TypeError: 'OnlyKeys' object is not subscriptable
+
+    >>> class BrokenKeys:
+    ...     def keys(self):
+    ...         return 1
+    >>> {**BrokenKeys()}
+    Traceback (most recent call last):
+      ...
+    TypeError: test.test_unpack_ex.BrokenKeys.keys() must return an iterable, not int
+
     >>> len(eval("{" + ", ".join("**{{{}: {}}}".format(i, i)
     ...                          for i in range(1000)) + "}"))
     1000
@@ -560,6 +576,176 @@ Some size constraints (all fail.)
 
 """
 
+def test_errors_in_iter():
+    """
+    >>> class A:
+    ...     def __iter__(self):
+    ...         raise exc
+    ...
+    >>> exc = ZeroDivisionError('some error')
+    >>> [*A()]
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> {*A()}
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> [*A()]
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> {*A()}
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> [*A()]
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+
+    >>> {*A()}
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_next():
+    """
+    >>> class I:
+    ...     def __iter__(self):
+    ...         return self
+    ...     def __next__(self):
+    ...         raise exc
+    ...
+    >>> class A:
+    ...     def __iter__(self):
+    ...         return I()
+    ...
+
+    >>> exc = ZeroDivisionError('some error')
+    >>> [*A()]
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> {*A()}
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> [*A()]
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> {*A()}
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> [*A()]
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+
+    >>> {*A()}
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_keys():
+    """
+    >>> class D:
+    ...     def keys(self):
+    ...         raise exc
+    ...
+    >>> exc = ZeroDivisionError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_keys_next():
+    """
+    >>> class I:
+    ...     def __iter__(self):
+    ...         return self
+    ...     def __next__(self):
+    ...         raise exc
+    ...
+    >>> class D:
+    ...     def keys(self):
+    ...         return I()
+    ...
+    >>> exc = ZeroDivisionError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
+def test_errors_in_getitem():
+    """
+    >>> class D:
+    ...     def keys(self):
+    ...         return ['key']
+    ...     def __getitem__(self, key):
+    ...         raise exc
+    ...
+    >>> exc = ZeroDivisionError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    ZeroDivisionError: some error
+
+    >>> exc = AttributeError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    AttributeError: some error
+
+    >>> exc = TypeError('some error')
+    >>> {**D()}
+    Traceback (most recent call last):
+      ...
+    TypeError: some error
+    """
+
 __test__ = {'doctests' : doctests}
 
 def load_tests(loader, tests, pattern):
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst
new file mode 100644 (file)
index 0000000..7923d80
--- /dev/null
@@ -0,0 +1,3 @@
+:exc:`AttributeError`\ s raised in :meth:`!keys` or :meth:`!__getitem__`
+during dictionary unpacking (``{**mymapping}`` or ``func(**mymapping)``) are
+no longer masked by :exc:`TypeError`.
index 4b4457076acc549503f5a392c0d90371f3becd3c..68b0ad76f61cc0372ff565997ea77ddb442622fe 100644 (file)
                     stack_pointer = _PyFrame_GetStackPointer(frame);
                     if (matches) {
                         _PyFrame_SetStackPointer(frame, stack_pointer);
-                        _PyErr_Format(tstate, PyExc_TypeError,
-                                  "'%.200s' object is not a mapping",
-                                  Py_TYPE(update_o)->tp_name);
+                        PyObject *exc = _PyErr_GetRaisedException(tstate);
+                        int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys));
                         stack_pointer = _PyFrame_GetStackPointer(frame);
+                        if (has_keys == 0) {
+                            _PyFrame_SetStackPointer(frame, stack_pointer);
+                            _PyErr_Format(tstate, PyExc_TypeError,
+                                      "'%T' object is not a mapping",
+                                      update_o);
+                            Py_DECREF(exc);
+                            stack_pointer = _PyFrame_GetStackPointer(frame);
+                        }
+                        else {
+                            _PyFrame_SetStackPointer(frame, stack_pointer);
+                            _PyErr_ChainExceptions1(exc);
+                            stack_pointer = _PyFrame_GetStackPointer(frame);
+                        }
                     }
                     JUMP_TO_LABEL(error);
                 }
index 9cd5f85602447f3088f4897c07ee1616eaf5eb2c..09ac0441096b35c19e04e610a5e6db8da6bd674e 100644 (file)
@@ -2390,9 +2390,17 @@ dummy_func(
             if (err < 0) {
                 int matches = _PyErr_ExceptionMatches(tstate, PyExc_AttributeError);
                 if (matches) {
-                    _PyErr_Format(tstate, PyExc_TypeError,
-                                    "'%.200s' object is not a mapping",
-                                    Py_TYPE(update_o)->tp_name);
+                    PyObject *exc = _PyErr_GetRaisedException(tstate);
+                    int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys));
+                    if (has_keys == 0) {
+                        _PyErr_Format(tstate, PyExc_TypeError,
+                                        "'%T' object is not a mapping",
+                                        update_o);
+                        Py_DECREF(exc);
+                    }
+                    else {
+                        _PyErr_ChainExceptions1(exc);
+                    }
                 }
                 ERROR_NO_POP();
             }
index 2f9195529f2ceb6f9fabe822b25ab6233dde3d3f..b4c57b65d13d18ba0b43fc3112661185ee8489d6 100644 (file)
@@ -3452,10 +3452,18 @@ _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwarg
      * is not a mapping.
      */
     if (_PyErr_ExceptionMatches(tstate, PyExc_AttributeError)) {
-        _PyErr_Format(
-            tstate, PyExc_TypeError,
-            "Value after ** must be a mapping, not %.200s",
-            Py_TYPE(kwargs)->tp_name);
+        PyObject *exc = _PyErr_GetRaisedException(tstate);
+        int has_keys = PyObject_HasAttrWithError(kwargs, &_Py_ID(keys));
+        if (has_keys == 0) {
+            _PyErr_Format(
+                tstate, PyExc_TypeError,
+                "Value after ** must be a mapping, not %T",
+                kwargs);
+            Py_DECREF(exc);
+        }
+        else {
+            _PyErr_ChainExceptions1Tstate(tstate, exc);
+        }
     }
     else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
         PyObject *exc = _PyErr_GetRaisedException(tstate);
index 6b3224ef58b6ba0d59f90fdb75d00872b5f0aa1c..ff1feac47d1b34d140a344f292d52b2beac7f493 100644 (file)
                 stack_pointer = _PyFrame_GetStackPointer(frame);
                 if (matches) {
                     _PyFrame_SetStackPointer(frame, stack_pointer);
-                    _PyErr_Format(tstate, PyExc_TypeError,
-                                  "'%.200s' object is not a mapping",
-                                  Py_TYPE(update_o)->tp_name);
+                    PyObject *exc = _PyErr_GetRaisedException(tstate);
+                    int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys));
                     stack_pointer = _PyFrame_GetStackPointer(frame);
+                    if (has_keys == 0) {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        _PyErr_Format(tstate, PyExc_TypeError,
+                                      "'%T' object is not a mapping",
+                                      update_o);
+                        Py_DECREF(exc);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                    }
+                    else {
+                        _PyFrame_SetStackPointer(frame, stack_pointer);
+                        _PyErr_ChainExceptions1(exc);
+                        stack_pointer = _PyFrame_GetStackPointer(frame);
+                    }
                 }
                 SET_CURRENT_CACHED_VALUES(0);
                 JUMP_TO_ERROR();
index 0c0cbc8a6bb95ba7ce726f09464875b484098c9c..522c14014a6c3128337368c614c4d8fe01b5372a 100644 (file)
                     stack_pointer = _PyFrame_GetStackPointer(frame);
                     if (matches) {
                         _PyFrame_SetStackPointer(frame, stack_pointer);
-                        _PyErr_Format(tstate, PyExc_TypeError,
-                                  "'%.200s' object is not a mapping",
-                                  Py_TYPE(update_o)->tp_name);
+                        PyObject *exc = _PyErr_GetRaisedException(tstate);
+                        int has_keys = PyObject_HasAttrWithError(update_o, &_Py_ID(keys));
                         stack_pointer = _PyFrame_GetStackPointer(frame);
+                        if (has_keys == 0) {
+                            _PyFrame_SetStackPointer(frame, stack_pointer);
+                            _PyErr_Format(tstate, PyExc_TypeError,
+                                      "'%T' object is not a mapping",
+                                      update_o);
+                            Py_DECREF(exc);
+                            stack_pointer = _PyFrame_GetStackPointer(frame);
+                        }
+                        else {
+                            _PyFrame_SetStackPointer(frame, stack_pointer);
+                            _PyErr_ChainExceptions1(exc);
+                            stack_pointer = _PyFrame_GetStackPointer(frame);
+                        }
                     }
                     JUMP_TO_LABEL(error);
                 }