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]