diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 16913289a02f59..2ed674335ef652 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -311,7 +311,7 @@ PyAPI_FUNC(int) _PyEval_ExceptionGroupMatch(_PyInterpreterFrame *, PyObject* exc PyAPI_FUNC(void) _PyEval_FormatAwaitableError(PyThreadState *tstate, PyTypeObject *type, int oparg); PyAPI_FUNC(void) _PyEval_FormatExcCheckArg(PyThreadState *tstate, PyObject *exc, const char *format_str, PyObject *obj); PyAPI_FUNC(void) _PyEval_FormatExcUnbound(PyThreadState *tstate, PyCodeObject *co, int oparg); -PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs); +PyAPI_FUNC(void) _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey); PyAPI_FUNC(PyObject *) _PyEval_ImportFrom(PyThreadState *, PyObject *, PyObject *); PyAPI_FUNC(PyObject *) _PyEval_LazyImportName( diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 6d7d68eda84c5a..a1d1e10ec5c022 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -55,7 +55,7 @@ extern Py_ssize_t _PyDict_SizeOf_LockHeld(PyDictObject *); of a key wins, if override is 2, a KeyError with conflicting key as argument is raised. */ -PyAPI_FUNC(int) _PyDict_MergeEx(PyObject *mp, PyObject *other, int override); +PyAPI_FUNC(int) _PyDict_MergeUniq(PyObject *mp, PyObject *other, PyObject **dupkey); extern void _PyDict_DebugMallocStats(FILE *out); diff --git a/Lib/test/test_extcall.py b/Lib/test/test_extcall.py index 20dd16dd05d12d..4da5601e80295f 100644 --- a/Lib/test/test_extcall.py +++ b/Lib/test/test_extcall.py @@ -634,11 +634,11 @@ def test_errors_in_keys(): ... AttributeError: some error - >>> exc = TypeError('some error') + >>> exc = KeyError('some error') >>> f(**D()) Traceback (most recent call last): ... - TypeError: some error + KeyError: 'some error' """ def test_errors_in_keys_next(): @@ -666,11 +666,11 @@ def test_errors_in_keys_next(): ... AttributeError: some error - >>> exc = TypeError('some error') + >>> exc = KeyError('some error') >>> f(**D()) Traceback (most recent call last): ... - TypeError: some error + KeyError: 'some error' """ def test_errors_in_getitem(): @@ -694,11 +694,11 @@ def test_errors_in_getitem(): ... AttributeError: some error - >>> exc = TypeError('some error') + >>> exc = KeyError('some error') >>> f(**D()) Traceback (most recent call last): ... - TypeError: some error + KeyError: 'some error' """ import doctest diff --git a/Lib/test/test_unpack_ex.py b/Lib/test/test_unpack_ex.py index d3ba8133c41d57..33c96b84964b59 100644 --- a/Lib/test/test_unpack_ex.py +++ b/Lib/test/test_unpack_ex.py @@ -681,11 +681,11 @@ def test_errors_in_keys(): ... AttributeError: some error - >>> exc = TypeError('some error') + >>> exc = KeyError('some error') >>> {**D()} Traceback (most recent call last): ... - TypeError: some error + KeyError: 'some error' """ def test_errors_in_keys_next(): @@ -712,11 +712,11 @@ def test_errors_in_keys_next(): ... AttributeError: some error - >>> exc = TypeError('some error') + >>> exc = KeyError('some error') >>> {**D()} Traceback (most recent call last): ... - TypeError: some error + KeyError: 'some error' """ def test_errors_in_getitem(): @@ -739,11 +739,11 @@ def test_errors_in_getitem(): ... AttributeError: some error - >>> exc = TypeError('some error') + >>> exc = KeyError('some error') >>> {**D()} Traceback (most recent call last): ... - TypeError: some error + KeyError: 'some error' """ __test__ = {'doctests' : doctests} 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 index 7923d80953fd53..86579634fa14ce 100644 --- 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 @@ -1,3 +1,3 @@ -:exc:`AttributeError`\ s raised in :meth:`!keys` or :meth:`!__getitem__` +:exc:`AttributeError`\ s and :exc:`KeyError`\ 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 68b0ad76f61cc0..88f100da7ce2fa 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -5558,12 +5558,14 @@ PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict); PyObject *update_o = PyStackRef_AsPyObjectBorrow(update); + PyObject *dupkey = NULL; _PyFrame_SetStackPointer(frame, stack_pointer); - int err = _PyDict_MergeEx(dict_o, update_o, 2); + int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey); stack_pointer = _PyFrame_GetStackPointer(frame); if (err < 0) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyEval_FormatKwargsError(tstate, callable_o, update_o); + _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey); + Py_XDECREF(dupkey); stack_pointer = _PyFrame_GetStackPointer(frame); JUMP_TO_LABEL(error); } diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 08e40bf84c42fa..0c44493c5ea1c2 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -139,7 +139,7 @@ As a consequence of this, split keys have a maximum size of 16. static PyObject* frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -static int dict_merge(PyObject *a, PyObject *b, int override); +static int dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey); static int dict_contains(PyObject *op, PyObject *key); static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override); @@ -3391,7 +3391,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_DECREF(d); return NULL; } - if (dict_merge(copy, d, 1) < 0) { + if (dict_merge(copy, d, 1, NULL) < 0) { Py_DECREF(d); Py_DECREF(copy); return NULL; @@ -3887,14 +3887,14 @@ static int dict_update_arg(PyObject *self, PyObject *arg) { if (PyAnyDict_CheckExact(arg)) { - return dict_merge(self, arg, 1); + return dict_merge(self, arg, 1, NULL); } int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys)); if (has_keys < 0) { return -1; } if (has_keys) { - return dict_merge(self, arg, 1); + return dict_merge(self, arg, 1, NULL); } return dict_merge_from_seq2(self, arg, 1); } @@ -3915,7 +3915,7 @@ dict_update_common(PyObject *self, PyObject *args, PyObject *kwds, if (result == 0 && kwds != NULL) { if (PyArg_ValidateKeywordArguments(kwds)) - result = dict_merge(self, kwds, 1); + result = dict_merge(self, kwds, 1, NULL); else result = -1; } @@ -4059,7 +4059,7 @@ PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override) } static int -dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override) +dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override, PyObject **dupkey) { assert(can_modify_dict(mp)); ASSERT_DICT_LOCKED(other); @@ -4068,10 +4068,10 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override) /* a.update(a) or a.update({}); nothing to do */ return 0; if (mp->ma_used == 0) { - /* Since the target dict is empty, PyDict_GetItem() - * always returns NULL. Setting override to 1 - * skips the unnecessary test. - */ + /* Since the target dict is empty, _PyDict_Contains_KnownHash() + * always returns 0. Setting override to 1 + * skips the unnecessary test. + */ override = 1; PyDictKeysObject *okeys = other->ma_keys; @@ -4131,11 +4131,10 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override) err = insertdict(mp, Py_NewRef(key), hash, Py_NewRef(value)); } else if (err > 0) { - if (override != 0) { - _PyErr_SetKeyError(key); + if (dupkey != NULL) { + *dupkey = key; Py_DECREF(value); - Py_DECREF(key); - return -1; + return -2; } err = 0; } @@ -4155,7 +4154,7 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override) } static int -dict_merge(PyObject *a, PyObject *b, int override) +dict_merge(PyObject *a, PyObject *b, int override, PyObject **dupkey) { assert(a != NULL); assert(b != NULL); @@ -4167,7 +4166,7 @@ dict_merge(PyObject *a, PyObject *b, int override) PyDictObject *other = (PyDictObject*)b; int res; Py_BEGIN_CRITICAL_SECTION2(a, b); - res = dict_dict_merge((PyDictObject *)a, other, override); + res = dict_dict_merge((PyDictObject *)a, other, override, dupkey); ASSERT_CONSISTENT(a); Py_END_CRITICAL_SECTION2(); return res; @@ -4202,15 +4201,18 @@ dict_merge(PyObject *a, PyObject *b, int override) status = dict_contains(a, key); if (status != 0) { if (status > 0) { - if (override == 0) { + if (dupkey == NULL) { Py_DECREF(key); continue; } - _PyErr_SetKeyError(key); + *dupkey = key; + res = -2; + } + else { + Py_DECREF(key); + res = -1; } - Py_DECREF(key); Py_DECREF(iter); - res = -1; goto slow_exit; } } @@ -4246,7 +4248,7 @@ dict_merge(PyObject *a, PyObject *b, int override) } static int -dict_merge_api(PyObject *a, PyObject *b, int override) +dict_merge_api(PyObject *a, PyObject *b, int override, PyObject **dupkey) { /* We accept for the argument either a concrete dictionary object, * or an abstract "mapping" object. For the former, we can do @@ -4262,26 +4264,26 @@ dict_merge_api(PyObject *a, PyObject *b, int override) } return -1; } - return dict_merge(a, b, override); + return dict_merge(a, b, override, dupkey); } int PyDict_Update(PyObject *a, PyObject *b) { - return dict_merge_api(a, b, 1); + return dict_merge_api(a, b, 1, NULL); } int PyDict_Merge(PyObject *a, PyObject *b, int override) { /* XXX Deprecate override not in (0, 1). */ - return dict_merge_api(a, b, override != 0); + return dict_merge_api(a, b, override != 0, NULL); } int -_PyDict_MergeEx(PyObject *a, PyObject *b, int override) +_PyDict_MergeUniq(PyObject *a, PyObject *b, PyObject **dupkey) { - return dict_merge_api(a, b, override); + return dict_merge_api(a, b, 2, dupkey); } /*[clinic input] @@ -4421,7 +4423,7 @@ copy_lock_held(PyObject *o, int as_frozendict) } if (copy == NULL) return NULL; - if (dict_merge(copy, o, 1) == 0) + if (dict_merge(copy, o, 1, NULL) == 0) return copy; Py_DECREF(copy); return NULL; diff --git a/Python/bytecodes.c b/Python/bytecodes.c index 09ac0441096b35..4fd20aef76ce47 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2414,10 +2414,12 @@ dummy_func( PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict); PyObject *update_o = PyStackRef_AsPyObjectBorrow(update); + PyObject *dupkey = NULL; - int err = _PyDict_MergeEx(dict_o, update_o, 2); + int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey); if (err < 0) { - _PyEval_FormatKwargsError(tstate, callable_o, update_o); + _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey); + Py_XDECREF(dupkey); ERROR_NO_POP(); } u = update; diff --git a/Python/ceval.c b/Python/ceval.c index b4c57b65d13d18..91fad9a03abcee 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -3443,9 +3443,18 @@ _Py_Check_ArgsIterable(PyThreadState *tstate, PyObject *func, PyObject *args) } void -_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs) -{ - /* _PyDict_MergeEx raises attribute +_PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwargs, PyObject *dupkey) +{ + if (dupkey != NULL) { + PyObject *funcstr = _PyObject_FunctionStr(func); + _PyErr_Format( + tstate, PyExc_TypeError, + "%V got multiple values for keyword argument '%S'", + funcstr, "finction", dupkey); + Py_XDECREF(funcstr); + return; + } + /* _PyDict_MergeUniq raises attribute * error (percolated from an attempt * to get 'keys' attribute) instead of * a type error if its second argument @@ -3465,27 +3474,6 @@ _PyEval_FormatKwargsError(PyThreadState *tstate, PyObject *func, PyObject *kwarg _PyErr_ChainExceptions1Tstate(tstate, exc); } } - else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) { - PyObject *exc = _PyErr_GetRaisedException(tstate); - PyObject *args = PyException_GetArgs(exc); - if (PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1) { - _PyErr_Clear(tstate); - PyObject *funcstr = _PyObject_FunctionStr(func); - if (funcstr != NULL) { - PyObject *key = PyTuple_GET_ITEM(args, 0); - _PyErr_Format( - tstate, PyExc_TypeError, - "%U got multiple values for keyword argument '%S'", - funcstr, key); - Py_DECREF(funcstr); - } - Py_XDECREF(exc); - } - else { - _PyErr_SetRaisedException(tstate, exc); - } - Py_DECREF(args); - } } void diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index ff1feac47d1b34..3ec8d5556790a1 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -9464,15 +9464,17 @@ PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict); PyObject *update_o = PyStackRef_AsPyObjectBorrow(update); + PyObject *dupkey = NULL; stack_pointer[0] = update; stack_pointer += 1; ASSERT_WITHIN_STACK_BOUNDS(__FILE__, __LINE__); _PyFrame_SetStackPointer(frame, stack_pointer); - int err = _PyDict_MergeEx(dict_o, update_o, 2); + int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey); stack_pointer = _PyFrame_GetStackPointer(frame); if (err < 0) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyEval_FormatKwargsError(tstate, callable_o, update_o); + _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey); + Py_XDECREF(dupkey); 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 522c14014a6c31..fe3179b666468a 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -5558,12 +5558,14 @@ PyObject *callable_o = PyStackRef_AsPyObjectBorrow(callable); PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict); PyObject *update_o = PyStackRef_AsPyObjectBorrow(update); + PyObject *dupkey = NULL; _PyFrame_SetStackPointer(frame, stack_pointer); - int err = _PyDict_MergeEx(dict_o, update_o, 2); + int err = _PyDict_MergeUniq(dict_o, update_o, &dupkey); stack_pointer = _PyFrame_GetStackPointer(frame); if (err < 0) { _PyFrame_SetStackPointer(frame, stack_pointer); - _PyEval_FormatKwargsError(tstate, callable_o, update_o); + _PyEval_FormatKwargsError(tstate, callable_o, update_o, dupkey); + Py_XDECREF(dupkey); stack_pointer = _PyFrame_GetStackPointer(frame); JUMP_TO_LABEL(error); }