https://github.com/python/cpython/commit/0e543055b01203b7a26424675913de3284375a40
commit: 0e543055b01203b7a26424675913de3284375a40
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-03-26T13:48:57Z
summary:

gh-145876: Do not mask AttributeErrors raised during dictionary unpacking 
(GH-145906)

AttributeErrors raised in keys() or __getitem__() during
dictionary unpacking ({**mymapping} or func(**mymapping)) are
no longer masked by TypeError.

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst
M Lib/test/test_extcall.py
M Lib/test/test_unpack_ex.py
M Modules/_testinternalcapi/test_cases.c.h
M Python/bytecodes.c
M Python/ceval.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h

diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py
index f003a5837ae0eb..20dd16dd05d12d 100644
--- a/Lib/test/test_extcall.py
+++ b/Lib/test/test_extcall.py
@@ -329,6 +329,22 @@
       ...
     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 @@
 
 """
 
+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
 
diff --git a/Lib/test/test_unpack_ex.py b/Lib/test/test_unpack_ex.py
index 904cf4f626ae78..d3ba8133c41d57 100644
--- a/Lib/test/test_unpack_ex.py
+++ b/Lib/test/test_unpack_ex.py
@@ -134,6 +134,22 @@
     ...
     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 @@
 
 """
 
+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
index 00000000000000..7923d80953fd53
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-03-13-12-24-17.gh-issue-145876.LWFO2K.rst
@@ -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`.
diff --git a/Modules/_testinternalcapi/test_cases.c.h 
b/Modules/_testinternalcapi/test_cases.c.h
index 4b4457076acc54..68b0ad76f61cc0 100644
--- a/Modules/_testinternalcapi/test_cases.c.h
+++ b/Modules/_testinternalcapi/test_cases.c.h
@@ -5608,10 +5608,22 @@
                     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);
                 }
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 9cd5f85602447f..09ac0441096b35 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -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();
             }
diff --git a/Python/ceval.c b/Python/ceval.c
index 2f9195529f2ceb..b4c57b65d13d18 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -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);
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 6b3224ef58b6ba..ff1feac47d1b34 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -9418,10 +9418,22 @@
                 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();
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index 0c0cbc8a6bb95b..522c14014a6c31 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -5608,10 +5608,22 @@
                     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);
                 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to